Move feature enable switches to the top of Settings pages (#1144)

* Promote feature enable controls in Settings

* Reuse the shared feature toggle label

* Keep Search Scopes available when Applications is off

* Restore search highlight on launcher settings lists
This commit is contained in:
Clément Knoderer
2026-09-25 13:26:42 +06:00
committed by GitHub
parent c98b33159d
commit 33fc51c0dc
24 changed files with 162 additions and 76 deletions
+1
View File
@@ -55,6 +55,7 @@ for (const m of catalog.matchAll(/\.init\(\s*\.(\w+),\s*"((?:[^"\\]|\\.)*)"/g))
const quoted = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const marked =
source.includes(`SettingsRowTitle(.${anchor}, "${title}")`) ||
new RegExp(`SettingsFeatureToggleLabel\\(\\s*anchor: \\.${anchor}, title: "${quoted}"`).test(source) ||
// A `SettingsRow` renders the pill from its own title.
new RegExp(`SettingsRow\\(\\s*title: "${quoted}",[\\s\\S]*?anchor: \\.${anchor}`).test(source) ||
// A feature pane's master switch, rendered by `FeatureSwitchSection`.
+63 -3
View File
@@ -3,6 +3,51 @@ import SwiftUI
// The few pieces more than one Settings pane or editor needs; everything else stays feature-owned.
/// The Settings sidebar tile, shared with the matching feature switches.
struct SettingsTabIcon: View {
let systemImage: String
let tint: Color
var size = Theme.Size.settingsSidebarGlyph + Theme.Spacing.xs * 2
var body: some View {
let scale = size / (Theme.Size.settingsSidebarGlyph + Theme.Spacing.xs * 2)
Image(systemName: systemImage)
.resizable()
.scaledToFit()
.frame(
width: Theme.Size.settingsSidebarGlyph * scale,
height: Theme.Size.settingsSidebarGlyph * scale
)
.foregroundStyle(tint)
.padding(Theme.Spacing.xs * scale)
.background(
tint.opacity(0.1),
in: RoundedRectangle(
cornerRadius: Theme.Radius.thumbnail * scale, style: .continuous))
}
}
struct SettingsFeatureToggleLabel: View {
let anchor: SettingsAnchor
let title: String
let subtitle: String
var body: some View {
HStack(spacing: Theme.Spacing.lg) {
SettingsTabIcon(
systemImage: anchor.tab.systemImage, tint: .accentColor,
size: Theme.Size.settingsRowIcon * 1.5)
VStack(alignment: .leading, spacing: Theme.Spacing.xxs) {
SettingsRowTitle(anchor, title)
.fontWeight(.semibold)
Text(subtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}
}
/// Not `LabeledContent`: its selectable text field eats the taps a `ShortcutRecorder` needs.
struct SettingsRow<Icon: View, Trailing: View>: View {
let title: String
@@ -166,18 +211,33 @@ struct FeatureSwitchSection: View {
var enableSubtitle: String?
@Binding var isEnabled: Bool
@Binding var showsInLauncher: Bool
var showsIcon = false
var showsHeader = true
var body: some View {
if showsHeader {
section
} else {
section.settingsAnchor(anchor)
}
}
private var section: some View {
Section {
Toggle(isOn: $isEnabled) {
SettingsRowTitle(anchor, enableTitle)
if let enableSubtitle { Text(enableSubtitle) }
if showsIcon, let enableSubtitle {
SettingsFeatureToggleLabel(
anchor: anchor, title: enableTitle, subtitle: enableSubtitle)
} else {
SettingsRowTitle(anchor, enableTitle)
if let enableSubtitle { Text(enableSubtitle) }
}
}
Toggle("Show in launcher", isOn: $showsInLauncher)
// The switch above stays live so the feature can always be turned back on.
.settingsEnabled(isEnabled)
} header: {
SettingsSectionHeader(anchor)
if showsHeader { SettingsSectionHeader(anchor) }
}
}
}
@@ -22,8 +22,9 @@ struct AISettingsView: View {
return Form {
Section {
Toggle(isOn: $appSettings.aiEnabled) {
SettingsRowTitle(.aiAI, "Enable AI")
Text("Nothing is loaded or sent while it is off.")
SettingsFeatureToggleLabel(
anchor: .aiAI, title: "Enable AI",
subtitle: "Nothing is loaded or sent while it is off.")
}
SettingsRow(
title: "Providers", subtitle: providerSummary, anchor: .aiProviders
@@ -20,12 +20,13 @@ struct AppleShortcutsSettingsView: View {
return Form {
Section {
Toggle(isOn: $settings.appleShortcutsEnabled) {
SettingsRowTitle(.appleShortcutsAppleShortcuts, "Enable Apple Shortcuts")
Text("Run your shortcuts from the launcher.")
SettingsFeatureToggleLabel(
anchor: .appleShortcutsAppleShortcuts,
title: "Enable Apple Shortcuts",
subtitle: "Run your shortcuts from the launcher.")
}
} header: {
SettingsSectionHeader(.appleShortcutsAppleShortcuts)
}
.settingsAnchor(.appleShortcutsAppleShortcuts)
if settings.appleShortcutsEnabled {
library
@@ -15,7 +15,9 @@ struct CalendarSettingsView: View {
"Reads \(core.calendarCoordinator.span.possessivePhrase) events for join links. "
+ "Nothing leaves this Mac.",
isEnabled: enabledBinding,
showsInLauncher: $settings.calendarShowInLauncher)
showsInLauncher: $settings.calendarShowInLauncher,
showsIcon: true,
showsHeader: false)
Section {
Picker(selection: $settings.calendarLauncherLimit) {
@@ -11,11 +11,12 @@ struct ClipboardSettingsView: View {
return Form {
Section {
Toggle(isOn: $settings.clipboardEnabled) {
SettingsRowTitle(.clipboardClipboard, "Enable Clipboard History")
SettingsFeatureToggleLabel(
anchor: .clipboardClipboard, title: "Enable Clipboard History",
subtitle: "Keep copied items ready to reuse.")
}
} header: {
SettingsSectionHeader(.clipboardClipboard)
}
.settingsAnchor(.clipboardClipboard)
FeatureCommandsSection(owner: .clipboard, anchor: .clipboardCommands)
.settingsEnabled(settings.clipboardEnabled)
@@ -11,6 +11,8 @@ struct CommandsSettingsView: View {
var body: some View {
@Bindable var settings = settings
return Form {
LauncherCategorySwitchSection(kind: .command, anchor: .commandsCommands)
LauncherItemsSection(
kind: .command,
anchor: .commandsCommands,
@@ -28,7 +28,8 @@ struct ExtensionsSettingsView: View {
isEnabled: Binding(
get: { settings.extensionsEnabled },
set: { core.extensionCoordinator.setExtensionsEnabled($0) }),
showsInLauncher: $settings.extensionsShowInLauncher)
showsInLauncher: $settings.extensionsShowInLauncher,
showsIcon: true)
Group {
install
@@ -8,12 +8,12 @@ struct FileSearchSettingsView: View {
return Form {
Section {
Toggle(isOn: $settings.fileSearchEnabled) {
SettingsRowTitle(.fileSearchFileSearch, "Enable File Search")
Text("Uses the Spotlight index, only when you search.")
SettingsFeatureToggleLabel(
anchor: .fileSearchFileSearch, title: "Enable File Search",
subtitle: "Uses the Spotlight index, only when you search.")
}
} header: {
SettingsSectionHeader(.fileSearchFileSearch)
}
.settingsAnchor(.fileSearchFileSearch)
FeatureCommandsSection(owner: .fileSearch, anchor: .fileSearchCommands)
.settingsEnabled(settings.fileSearchEnabled)
@@ -3,7 +3,9 @@ import SwiftUI
struct ApplicationsSettingsView: View {
var body: some View {
Form {
// Scopes first: they decide what gets indexed, so they read before the results.
LauncherCategorySwitchSection(
kind: .application, anchor: .applicationsApplications)
SearchScopesSection()
LauncherItemsSection(
@@ -15,4 +17,5 @@ struct ApplicationsSettingsView: View {
.settingsScrollTarget(.applications)
.releasesFocusOnOutsideClick()
}
}
@@ -1,6 +1,29 @@
import SwiftUI
/// One category's Settings sections; never filters by visibility, so hidden rows stay listed.
/// A category's master switch stays available while its list is disabled.
struct LauncherCategorySwitchSection: View {
let kind: AppEntry.Kind
let anchor: SettingsAnchor
@Environment(VisibilityStore.self) private var visibility
var body: some View {
Section {
Toggle(
isOn: Binding(
get: { visibility.isKindEnabled(kind) },
set: { visibility.setKindEnabled($0, for: kind) }
)
) {
SettingsFeatureToggleLabel(
anchor: anchor, title: "Enable \(anchor.title)",
subtitle: "Off hides all of them and stops their shortcuts.")
}
}
}
}
/// One category's Settings list; never filters by visibility, so hidden rows stay listed.
struct LauncherItemsSection: View {
let kind: AppEntry.Kind
let anchor: SettingsAnchor
@@ -19,29 +42,15 @@ struct LauncherItemsSection: View {
}
var body: some View {
Section {
Toggle(isOn: enabledBinding) {
SettingsRowTitle(anchor, "Enable \(anchor.title)")
Text("Off hides all of them and stops their shortcuts.")
}
} header: {
SettingsSectionHeader(anchor)
}
Section {
SettingsFilterField(prompt: searchPrompt, query: $query)
LauncherItemsList(
entries: entries, query: query, isEnabled: visibility.isKindEnabled(kind))
} header: {
SettingsSectionHeader(anchor)
}
.settingsEnabled(visibility.isKindEnabled(kind))
}
private var enabledBinding: Binding<Bool> {
Binding(
get: { visibility.isKindEnabled(kind) },
set: { visibility.setKindEnabled($0, for: kind) }
)
}
}
/// The rows under a filter field, or what to say when there are none; shared by item panes.
@@ -4,6 +4,9 @@ import SwiftUI
struct SystemSettingsSettingsView: View {
var body: some View {
Form {
LauncherCategorySwitchSection(
kind: .systemSettings, anchor: .systemSettingsSystemSettings)
LauncherItemsSection(
kind: .systemSettings,
anchor: .systemSettingsSystemSettings,
@@ -8,20 +8,25 @@ struct NotesSettingsView: View {
return Form {
Section {
Toggle(isOn: $settings.notesEnabled) {
SettingsRowTitle(.notesNotes, "Enable Notes")
Text("Plain Markdown in a floating editor.")
SettingsFeatureToggleLabel(
anchor: .notesNotes, title: "Enable Notes",
subtitle: "Plain Markdown in a floating editor.")
}
}
.settingsAnchor(.notesNotes)
Section {
Toggle(isOn: $settings.notesRendersMarkdown) {
SettingsRowTitle(.notesNotes, "Render Markdown")
SettingsRowTitle(.notesOptions, "Render Markdown")
Text("Formats as you type.")
}
.settingsEnabled(settings.notesEnabled)
Toggle(isOn: $settings.notesShowsFormattingBar) {
SettingsRowTitle(.notesNotes, "Show Formatting Bar")
SettingsRowTitle(.notesOptions, "Show Formatting Bar")
}
.settingsEnabled(settings.notesEnabled && settings.notesRendersMarkdown)
} header: {
SettingsSectionHeader(.notesNotes)
SettingsSectionHeader(.notesOptions)
}
FeatureCommandsSection(owner: .notes, anchor: .notesCommands)
@@ -20,8 +20,9 @@ struct QuickActionsSettingsView: View {
Form {
Section {
Toggle(isOn: enabledBinding) {
SettingsRowTitle(.quickActionsQuickActions, "Enable Quick Actions")
Text("Act on selected text. Nothing is read until you press a shortcut.")
SettingsFeatureToggleLabel(
anchor: .quickActionsQuickActions, title: "Enable Quick Actions",
subtitle: "Act on selected text. Nothing is read until you press a shortcut.")
}
if appSettings.quickActionsEnabled, !isTrusted {
// Every shortcut fails without it; better said here than found one press later.
@@ -15,8 +15,11 @@ struct QuicklinksSettingsView: View {
FeatureSwitchSection(
anchor: .quicklinksQuicklinks,
enableTitle: "Enable quicklinks",
enableSubtitle: "Open saved links and searches from the launcher.",
isEnabled: $settings.quicklinksEnabled,
showsInLauncher: $settings.quicklinksShowInLauncher)
showsInLauncher: $settings.quicklinksShowInLauncher,
showsIcon: true,
showsHeader: false)
Group {
if !store.isAvailable { storageNotice }
@@ -10,12 +10,12 @@ struct NavigationSettingsView: View {
return Form {
Section {
Toggle(isOn: $settings.navigationEnabled) {
SettingsRowTitle(.navigationNavigation, "Enable navigation")
Text("Switch windows and search menu bar items.")
SettingsFeatureToggleLabel(
anchor: .navigationNavigation, title: "Enable navigation",
subtitle: "Switch windows and search menu bar items.")
}
} header: {
SettingsSectionHeader(.navigationNavigation)
}
.settingsAnchor(.navigationNavigation)
// No "show in launcher" switch: the per-command checkboxes below already are one.
FeatureCommandsSection(
@@ -57,6 +57,7 @@ extension SettingsAnchor {
static let fileSearchIgnorePatterns = Self(tab: .fileSearch, title: "Ignore Patterns")
static let notesNotes = Self(tab: .notes, title: "Notes")
static let notesOptions = Self(tab: .notes, title: "Options")
static let notesCommands = Self(tab: .notes, title: "Commands")
static let snippetsSnippets = Self(tab: .snippets, title: "Snippets")
@@ -176,12 +176,12 @@ enum SettingsSearchCatalog {
private static let applications: [SettingsSearchEntry] = [
.init(pane: .applications, keywords: ["apps", "index", "launcher"]),
.init(
group: .applicationsSearchScopes, "Search Scopes",
keywords: ["folders", "indexed", "locations", "add folder"]),
.init(
.applicationsApplications, "Enable Applications",
keywords: ["hide apps", "visibility"]),
.init(
group: .applicationsSearchScopes, "Search Scopes",
keywords: ["folders", "indexed", "locations", "add folder"]),
.init(
group: .applicationsApplications, "Aliases and shortcuts",
keywords: ["alias", "hotkey", "per app", "hide"])
@@ -349,10 +349,10 @@ enum SettingsSearchCatalog {
.notesNotes, "Enable Notes",
keywords: ["markdown", "scratchpad"]),
.init(
.notesNotes, "Render Markdown",
.notesOptions, "Render Markdown",
keywords: ["markdown", "formatting", "preview", "raw", "source"]),
.init(
.notesNotes, "Show Formatting Bar",
.notesOptions, "Show Formatting Bar",
keywords: ["toolbar", "format bar", "buttons", "bold", "heading", "markdown"]),
.init(
group: .notesCommands, "Notes commands",
@@ -117,21 +117,3 @@ private struct CenteredLabelStyle: LabelStyle {
}
}
}
/// The glyph on a tinted tile, so every row's icon reads at one weight whatever its symbol's shape.
private struct SettingsTabIcon: View {
let systemImage: String
let tint: Color
var body: some View {
Image(systemName: systemImage)
.resizable()
.scaledToFit()
.frame(width: Theme.Size.settingsSidebarGlyph, height: Theme.Size.settingsSidebarGlyph)
.foregroundStyle(tint)
.padding(Theme.Spacing.xs)
.background(
tint.opacity(0.1),
in: RoundedRectangle(cornerRadius: Theme.Radius.thumbnail, style: .continuous))
}
}
@@ -19,7 +19,9 @@ struct SnippetsSettingsView: View {
isEnabled: Binding(
get: { settings.snippetsEnabled },
set: { core.snippetCoordinator.setSnippetsEnabled($0) }),
showsInLauncher: $settings.snippetsShowInLauncher)
showsInLauncher: $settings.snippetsShowInLauncher,
showsIcon: true,
showsHeader: false)
if settings.snippetsEnabled, core.snippetListener.status == .needsAccessibility {
Section {
@@ -3,6 +3,9 @@ import SwiftUI
struct SystemActionsSettingsView: View {
var body: some View {
Form {
LauncherCategorySwitchSection(
kind: .systemAction, anchor: .systemActionsSystemActions)
LauncherItemsSection(
kind: .systemAction,
anchor: .systemActionsSystemActions,
@@ -15,7 +15,9 @@ struct WindowManagementSettingsView: View {
enableTitle: "Enable window management",
enableSubtitle: "Moves the last window you used. Needs Accessibility.",
isEnabled: $settings.windowManagementEnabled,
showsInLauncher: $settings.windowManagementShowInLauncher)
showsInLauncher: $settings.windowManagementShowInLauncher,
showsIcon: true,
showsHeader: false)
Group {
options
+4 -3
View File
@@ -11,9 +11,10 @@ earliest scope wins).
`LauncherList.rows`, in that order.
- **A category's switch is a master switch, not a list filter.** `VisibilityStore.isKindEnabled` gates
`orderedResults` *and* `HotKeyManager.perform`, so `Enable Applications` off stops the per-app chords
as well as the rows — the guard sits in the one dispatch funnel, the way each feature switch already
guards its own. The per-item checkbox beside it is the narrow tool: it hides one row and leaves that
row's shortcut firing, and **Hide from Search** in the ⌘K menu ticks that same checkbox off for the
as well as the rows. Its Settings switch stays available while the application
list disables beneath it — the guard sits in the one dispatch funnel, the way each feature switch
already guards its own. The per-item checkbox beside it is the narrow tool: it hides one row and
leaves that row's shortcut firing, and **Hide from Search** in the ⌘K menu ticks that checkbox off for the
kinds whose pane can tick it back on. A new category must be wired into
`VisibilityStore.allowsHotKey`, or its chords keep running while its pane reads off.
- **One command, one pane, one switch.** `SettingsTab.ownedCommands` is the whole table of which pane
+2
View File
@@ -660,6 +660,8 @@ system-drawn and a pane reads exactly as macOS System Settings does.
titlebar they were tuned for. Never hand-draw a header band; a main surface takes the system's
material, not `glassEffect`.
- `SettingsComponents.swift` holds only what more than one pane or editor needs: **`SettingsRow`**,
**`SettingsTabIcon`** (the sidebar tile reused by feature switches),
**`SettingsFeatureToggleLabel`** (the icon, title and subtitle of a feature's master switch),
**`FeatureSwitchSection`** (a feature's master switch plus its launcher-visibility companion),
**`SettingsFilterField`** (the filter row above a long list), **`launcherVisibilityHelp()`**, and the
Settings editor header, fields and surface. `ModalActionButtonStyle.swift` keeps every borderless surface's actions on one