Merge pull request #34 from RobotsAndPencils/list-style-update

List style update
This commit is contained in:
Brandon Evans 2020-12-30 23:17:15 -07:00 committed by GitHub
commit 38756100b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 248 additions and 103 deletions

View file

@ -49,3 +49,18 @@ Uninstallation is not provided yet. I had this partially implemented (one attemp
- [erikberglund/SwiftPrivilegedHelper](https://github.com/erikberglund/SwiftPrivilegedHelper)
- [aronskaya/smjobbless](https://github.com/aronskaya/smjobbless)
- [securing/SimpleXPCApp](https://github.com/securing/SimpleXPCApp)
## Selecting the active version of Xcode
This isn't a technical decision, but we spent enough time talking about this that it's probably worth sharing. When a user has more than one version of Xcode installed, a specific version of the developer tools can be selected with the `xcode-select` tool. The selected version of tools like xcodebuild or xcrun will be used unless the DEVELOPER_DIR environment variable has been set to a different path. You can read more about this in the `xcode-select` man pages. Notably, the man pages and [some notarization documentation](https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution) use the term "active" to indicate the Xcode version that's been selected. [This](https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-HOW_DO_I_SELECT_THE_DEFAULT_VERSION_OF_XCODE_TO_USE_FOR_MY_COMMAND_LINE_TOOLS_) older tech note uses the term "default". And of course, the `xcode-select` tool has the term "select" in its name. xcodes used the terms "select" and "selected" for this functionality, intending to match the xcode-select tool.
Here are the descriptions of these terms from [Apple's Style Guide](https://books.apple.com/ca/book/apple-style-guide/id1161855204):
> active: Use to refer to the app or window currently being used. Preferred to in front.
> default: OK to use to describe the state of settings before the user changes them. See also preset
> preset: Use to refer to a group of customized settings an app provides or the user saves for reuse.
> select: Use select, not choose, to refer to the action users perform when they select among multiple objects
Xcodes.app has this same functionality as xcodes, which still uses `xcode-select` under the hood, but because the main UI is a list of selectable rows, there _may_ be some ambiguity about the meaning of "selected". "Default" has a less clear connection to `xcode-select`'s name, but does accurately describe the behaviour that results. In Xcode 11 Launch Services also uses the selected Xcode version when opening a (GUI) developer tool bundled with Xcode, like Instruments. We could also try to follow Apple's lead by using the term "active" from the `xcode-select` man pages and notarization documentation. According to the style guide "active" already has a clear meaning in a GUI context.
Ultimately, we've decided to align with Apple's usage of "active" and "make active" in this specific context, despite possible confusion with the definition in the style guide.

View file

@ -75,8 +75,9 @@
CAFBDB912598FE80003DCC5A /* SelectedXcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDB902598FE80003DCC5A /* SelectedXcode.swift */; };
CAFBDB952598FE96003DCC5A /* FocusedValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDB942598FE96003DCC5A /* FocusedValues.swift */; };
CAFBDC4E2599B33D003DCC5A /* MainToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDC4D2599B33D003DCC5A /* MainToolbar.swift */; };
CAFBDC68259A308B003DCC5A /* InspectorPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDC67259A308B003DCC5A /* InspectorPane.swift */; };
CAFBDC68259A308B003DCC5A /* InfoPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDC67259A308B003DCC5A /* InfoPane.swift */; };
CAFBDC6C259A3098003DCC5A /* View+Conditional.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDC6B259A3098003DCC5A /* View+Conditional.swift */; };
CAFFFED8259CDA5000903F81 /* XcodeListViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -199,8 +200,9 @@
CAFBDB942598FE96003DCC5A /* FocusedValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusedValues.swift; sourceTree = "<group>"; };
CAFBDBA525990C76003DCC5A /* SimpleXPCApp.LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = SimpleXPCApp.LICENSE; sourceTree = "<group>"; };
CAFBDC4D2599B33D003DCC5A /* MainToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainToolbar.swift; sourceTree = "<group>"; };
CAFBDC67259A308B003DCC5A /* InspectorPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorPane.swift; sourceTree = "<group>"; };
CAFBDC67259A308B003DCC5A /* InfoPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoPane.swift; sourceTree = "<group>"; };
CAFBDC6B259A3098003DCC5A /* View+Conditional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Conditional.swift"; sourceTree = "<group>"; };
CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeListViewRow.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -305,11 +307,12 @@
isa = PBXGroup;
children = (
CA39711824495F0E00AFFB77 /* AppStoreButtonStyle.swift */,
CAFBDC67259A308B003DCC5A /* InspectorPane.swift */,
CAFBDC67259A308B003DCC5A /* InfoPane.swift */,
CAFBDC4D2599B33D003DCC5A /* MainToolbar.swift */,
CA44901E2463AD34003D8213 /* Tag.swift */,
CAE42486259A68A300B8B246 /* XcodeListCategory.swift */,
CAD2E7A32449574E00113D76 /* XcodeListView.swift */,
CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */,
);
path = XcodeList;
sourceTree = "<group>";
@ -651,10 +654,11 @@
CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */,
CAFBDB952598FE96003DCC5A /* FocusedValues.swift in Sources */,
CABFA9C22592EEEA00380FEE /* Promise+.swift in Sources */,
CAFBDC68259A308B003DCC5A /* InspectorPane.swift in Sources */,
CAFBDC68259A308B003DCC5A /* InfoPane.swift in Sources */,
CAA1CB4D255A5CFD003FD669 /* SignInPhoneListView.swift in Sources */,
CAFBDC6C259A3098003DCC5A /* View+Conditional.swift in Sources */,
CABFA9CF2592EEEA00380FEE /* Process.swift in Sources */,
CAFFFED8259CDA5000903F81 /* XcodeListViewRow.swift in Sources */,
CABFA9C72592EEEA00380FEE /* Entry+.swift in Sources */,
CAE42487259A68A300B8B246 /* XcodeListCategory.swift in Sources */,
CABFAA2C2592FBFC00380FEE /* SettingsView.swift in Sources */,

View file

@ -58,7 +58,11 @@ struct SelectButton: View {
var body: some View {
Button(action: select) {
Text("Select")
if xcode?.selected == true {
Text("Active")
} else {
Text("Make active")
}
}
.disabled(xcode?.selected != false)
.help("Select")

View file

@ -14,7 +14,7 @@ struct MainWindow: View {
.frame(minWidth: 300)
.layoutPriority(1)
InspectorPane(selectedXcodeID: selectedXcodeID)
InfoPane(selectedXcodeID: selectedXcodeID)
.frame(minWidth: 300, maxWidth: .infinity)
.frame(width: isShowingInfoPane ? nil : 0)
.isHidden(!isShowingInfoPane)

View file

@ -1,75 +1,137 @@
import SwiftUI
struct AppStoreButtonStyle: ButtonStyle {
var installed: Bool
var primary: Bool
var highlighted: Bool
private struct AppStoreButton: View {
@SwiftUI.Environment(\.isEnabled) var isEnabled
var configuration: ButtonStyle.Configuration
var installed: Bool
var primary: Bool
var highlighted: Bool
// This seems to magically help the highlight colors update on time
@SwiftUI.Environment(\.isFocused) var isFocused
var textColor: Color {
if installed {
if highlighted {
return Color.white
if isEnabled {
if primary {
if highlighted {
return Color.accentColor
}
else {
return Color.white
}
}
else {
return Color.secondary
}
}
else {
if highlighted {
return Color.accentColor
}
} else {
if primary {
if highlighted {
return Color(.disabledControlTextColor)
}
else {
return Color.white
}
}
else {
return Color.white
if highlighted {
return Color.white
}
else {
return Color(.disabledControlTextColor)
}
}
}
}
func background(isPressed: Bool) -> some View {
Group {
if installed {
EmptyView()
if isEnabled {
if primary {
Capsule()
.fill(
highlighted ?
Color.white :
Color.accentColor
)
.brightness(isPressed ? -0.25 : 0)
} else {
Capsule()
.fill(
Color(NSColor(red: 0.95, green: 0.95, blue: 0.97, alpha: 1))
)
.brightness(isPressed ? -0.25 : 0)
}
} else {
Capsule()
.fill(
highlighted ?
Color.white :
Color.accentColor
)
.brightness(isPressed ? -0.25 : 0)
if primary {
Capsule()
.fill(
highlighted ?
Color.white :
Color(.disabledControlTextColor)
)
.brightness(isPressed ? -0.25 : 0)
} else {
EmptyView()
}
}
}
}
var body: some View {
configuration.label
.font(Font.caption.weight(.medium))
.font(Font.caption.weight(.bold))
.foregroundColor(textColor)
.padding(EdgeInsets(top: 2, leading: 8, bottom: 2, trailing: 8))
.frame(minWidth: 80)
.padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8))
.frame(minWidth: 65)
.background(background(isPressed: configuration.isPressed))
.padding(1)
}
}
func makeBody(configuration: ButtonStyle.Configuration) -> some View {
AppStoreButton(configuration: configuration, installed: installed, highlighted: highlighted)
AppStoreButton(configuration: configuration, primary: primary, highlighted: highlighted)
}
}
struct AppStoreButtonStyle_Previews: PreviewProvider {
static var previews: some View {
Group {
Button("INSTALLED", action: {})
.buttonStyle(AppStoreButtonStyle(installed: true, highlighted: false))
.padding()
Button("INSTALL", action: {})
.buttonStyle(AppStoreButtonStyle(installed: false, highlighted: false))
.padding()
ForEach([ColorScheme.light, .dark], id: \.self) { colorScheme in
Group {
Button("OPEN", action: {})
.buttonStyle(AppStoreButtonStyle(primary: true, highlighted: false))
.padding()
.background(Color(.textBackgroundColor))
.previewDisplayName("Primary")
Button("OPEN", action: {})
.buttonStyle(AppStoreButtonStyle(primary: true, highlighted: true))
.padding()
.background(Color(.controlAccentColor))
.previewDisplayName("Primary, Highlighted")
Button("OPEN", action: {})
.buttonStyle(AppStoreButtonStyle(primary: true, highlighted: false))
.padding()
.disabled(true)
.background(Color(.textBackgroundColor))
.previewDisplayName("Primary, Disabled")
Button("INSTALL", action: {})
.buttonStyle(AppStoreButtonStyle(primary: false, highlighted: false))
.padding()
.background(Color(.textBackgroundColor))
.previewDisplayName("Secondary")
Button("INSTALL", action: {})
.buttonStyle(AppStoreButtonStyle(primary: false, highlighted: true))
.padding()
.background(Color(.controlAccentColor))
.previewDisplayName("Secondary, Highlighted")
Button("INSTALL", action: {})
.buttonStyle(AppStoreButtonStyle(primary: false, highlighted: false))
.padding()
.disabled(true)
.background(Color(.textBackgroundColor))
.previewDisplayName("Secondary, Disabled")
}
.environment(\.colorScheme, colorScheme)
}
}
}
}

View file

@ -4,7 +4,7 @@ import Version
import struct XCModel.SDKs
import struct XCModel.Compilers
struct InspectorPane: View {
struct InfoPane: View {
@EnvironmentObject var appState: AppState
let selectedXcodeID: Xcode.ID?
@SwiftUI.Environment(\.openURL) var openURL: OpenURLAction
@ -31,15 +31,12 @@ struct InspectorPane: View {
}
HStack {
if xcode.selected {
Button("Selected", action: {})
.disabled(true)
.help("Selected")
} else {
SelectButton(xcode: xcode)
}
SelectButton(xcode: xcode)
.disabled(xcode.selected)
.help("Selected")
OpenButton(xcode: xcode)
.help("Open")
}
} else {
InstallButton(xcode: xcode)
@ -170,10 +167,10 @@ struct InspectorPane: View {
}
}
struct InspectorPane_Previews: PreviewProvider {
struct InfoPane_Previews: PreviewProvider {
static var previews: some View {
Group {
InspectorPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 0))
InfoPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 0))
.environmentObject(configure(AppState()) {
$0.allXcodes = [
.init(
@ -201,7 +198,7 @@ struct InspectorPane_Previews: PreviewProvider {
})
.previewDisplayName("Populated, Installed, Selected")
InspectorPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 0))
InfoPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 0))
.environmentObject(configure(AppState()) {
$0.allXcodes = [
.init(
@ -227,7 +224,7 @@ struct InspectorPane_Previews: PreviewProvider {
})
.previewDisplayName("Populated, Installed, Unselected")
InspectorPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 0))
InfoPane(selectedXcodeID: Version(major: 12, minor: 3, patch: 0))
.environmentObject(configure(AppState()) {
$0.allXcodes = [
.init(
@ -253,7 +250,7 @@ struct InspectorPane_Previews: PreviewProvider {
})
.previewDisplayName("Populated, Uninstalled")
InspectorPane(selectedXcodeID: nil)
InfoPane(selectedXcodeID: nil)
.environmentObject(configure(AppState()) {
$0.allXcodes = [
]

View file

@ -40,10 +40,10 @@ struct MainToolbarModifier: ViewModifier {
Button(action: { isShowingInfoPane.toggle() }) {
if isShowingInfoPane {
Label("Inspector", systemImage: "info.circle.fill")
Label("Info", systemImage: "info.circle.fill")
.foregroundColor(.accentColor)
} else {
Label("Inspector", systemImage: "info.circle")
Label("Info", systemImage: "info.circle")
}
}
.keyboardShortcut(KeyboardShortcut("i", modifiers: [.command, .option]))

View file

@ -32,56 +32,7 @@ struct XcodeListView: View {
var body: some View {
List(visibleXcodes, selection: $selectedXcodeID) { xcode in
HStack {
appIconView(for: xcode)
VStack(alignment: .leading) {
Text(xcode.description)
.font(.body)
Text(verbatim: xcode.path ?? "")
.font(.caption)
.foregroundColor(selectedXcodeID == xcode.id ? Color(NSColor.selectedMenuItemTextColor) : Color(NSColor.secondaryLabelColor))
}
Spacer()
if xcode.selected {
Tag(text: "SELECTED")
.foregroundColor(.green)
}
Button(xcode.installed ? "INSTALLED" : "INSTALL") {
print("Installing...")
}
.buttonStyle(AppStoreButtonStyle(installed: xcode.installed,
highlighted: selectedXcodeID == xcode.id))
.disabled(xcode.installed)
}
.contextMenu {
InstallButton(xcode: xcode)
Divider()
if xcode.installed {
SelectButton(xcode: xcode)
OpenButton(xcode: xcode)
RevealButton(xcode: xcode)
CopyPathButton(xcode: xcode)
}
}
}
}
@ViewBuilder
func appIconView(for xcode: Xcode) -> some View {
if let icon = xcode.icon {
Image(nsImage: icon)
} else {
Color.clear
.frame(width: 32, height: 32)
.foregroundColor(.secondary)
XcodeListViewRow(xcode: xcode, selected: selectedXcodeID == xcode.id)
}
}
}

View file

@ -0,0 +1,112 @@
import SwiftUI
import Version
struct XcodeListViewRow: View {
@EnvironmentObject var appState: AppState
let xcode: Xcode
let selected: Bool
var body: some View {
HStack {
appIconView(for: xcode)
VStack(alignment: .leading) {
Text(xcode.description)
.font(.body)
Text(verbatim: xcode.path ?? "")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
selectControl(for: xcode)
.padding(.trailing, 16)
installControl(for: xcode)
}
.contextMenu {
InstallButton(xcode: xcode)
Divider()
if xcode.installed {
SelectButton(xcode: xcode)
OpenButton(xcode: xcode)
RevealButton(xcode: xcode)
CopyPathButton(xcode: xcode)
}
}
}
@ViewBuilder
func appIconView(for xcode: Xcode) -> some View {
if let icon = xcode.icon {
Image(nsImage: icon)
} else {
Color.clear
.frame(width: 32, height: 32)
.foregroundColor(.secondary)
}
}
@ViewBuilder
private func selectControl(for xcode: Xcode) -> some View {
if xcode.installed {
if xcode.selected {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
.help("This is the active version")
} else {
Button(action: { appState.select(id: xcode.id) }) {
Image(systemName: "checkmark.circle")
.foregroundColor(.secondary)
}
.buttonStyle(PlainButtonStyle())
.help("Make this the active version")
}
} else {
EmptyView()
}
}
@ViewBuilder
private func installControl(for xcode: Xcode) -> some View {
if xcode.installed {
Button("OPEN") { appState.open(id: xcode.id) }
.buttonStyle(AppStoreButtonStyle(primary: true, highlighted: selected))
.help("Open this version")
} else {
Button("INSTALL") { print("Installing...") }
.buttonStyle(AppStoreButtonStyle(primary: false, highlighted: selected))
.help("Install this version")
}
}
}
struct XcodeListViewRow_Previews: PreviewProvider {
static var previews: some View {
Group {
XcodeListViewRow(
xcode: Xcode(version: Version("12.3.0")!, installState: .installed, selected: true, path: "/Applications/Xcode-12.3.0.app", icon: nil),
selected: false
)
XcodeListViewRow(
xcode: Xcode(version: Version("12.2.0")!, installState: .notInstalled, selected: false, path: nil, icon: nil),
selected: false
)
XcodeListViewRow(
xcode: Xcode(version: Version("12.1.0")!, installState: .notInstalled, selected: false, path: nil, icon: nil),
selected: false
)
XcodeListViewRow(
xcode: Xcode(version: Version("12.0.0")!, installState: .installed, selected: false, path: "/Applications/Xcode-12.3.0.app", icon: nil),
selected: false
)
}
.environmentObject(AppState())
}
}