import AppKit import ScreenCaptureKit // ScreenSharePickerSheet — chooses *what* the SCREEN_AUDIO stream captures before sharing // starts. ScreenCaptureKit filters audio per application (not per window), so the user picks // a mode (everything / only-these / all-except-these) plus a set of apps, and a dedicated // toggle to drop their own screen-reader (VoiceOver) speech from the mix. // // Mirrors the modal-sheet pattern used by the rest of the macOS client (UserPickerSheet, // MoveUserSheet, …): an NSViewController presented via MainWindowController.presentSheet, with // an `onComplete` callback that returns the chosen `ScreenAudioSelection` (or nil on cancel). // // The app list comes from `SCShareableContent.current`, fetched asynchronously — that first // access is also what surfaces the Screen Recording (TCC) prompt, which is why the picker is // the natural place for it to appear, before any capture begins. final class ScreenSharePickerSheet: NSViewController, NSTableViewDataSource, NSTableViewDelegate { /// Called with the chosen selection, or `nil` if the user cancelled. var onComplete: ((ScreenAudioSelection?) -> Void)? private enum Mode: Int { case everything = 0, only = 1, except = 2 } private struct AppEntry { let name: String; let bundleID: String; let icon: NSImage? } private let initialSelection: ScreenAudioSelection private var mode: Mode private var excludeScreenReader: Bool private var checked: Set // bundle IDs ticked in the app table private var apps: [AppEntry] = [] private let tableView = NSTableView() private var modeControl: NSSegmentedControl? private var screenReaderCheckbox: NSButton? private var statusLabel: NSTextField? init(selection: ScreenAudioSelection) { self.initialSelection = selection switch selection.scope { case .entireDesktop: mode = .everything; checked = [] case .onlyApps(let ids): mode = .only; checked = Set(ids) case .allExcept(let ids): mode = .except; checked = Set(ids) } self.excludeScreenReader = selection.excludeScreenReader super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError() } override func loadView() { view = NSView(frame: NSRect(x: 0, y: 0, width: 360, height: 420)) } override func viewDidLoad() { super.viewDidLoad() buildUI() loadApps() } // MARK: - UI private func buildUI() { let titleLabel = NSTextField(labelWithString: "Choose what to share:") titleLabel.font = .boldSystemFont(ofSize: 13) titleLabel.setAccessibilityLabel("Choose what to share") let modeControl = NSSegmentedControl( labels: ["Everything", "Only selected", "All except selected"], trackingMode: .selectOne, target: self, action: #selector(modeChanged)) modeControl.selectedSegment = mode.rawValue modeControl.segmentDistribution = .fillEqually modeControl.setAccessibilityLabel("Share mode") self.modeControl = modeControl let col = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("app")) tableView.addTableColumn(col) tableView.headerView = nil tableView.dataSource = self tableView.delegate = self tableView.rowHeight = 22 tableView.setAccessibilityLabel("Application list") let scroll = NSScrollView() scroll.documentView = tableView scroll.hasVerticalScroller = true scroll.borderType = .bezelBorder scroll.translatesAutoresizingMaskIntoConstraints = false scroll.setContentHuggingPriority(.defaultLow, for: .vertical) let statusLabel = NSTextField(labelWithString: "Loading apps…") statusLabel.textColor = .secondaryLabelColor statusLabel.font = .systemFont(ofSize: 11) self.statusLabel = statusLabel let screenReaderCheckbox = NSButton(checkboxWithTitle: "Exclude screen reader (VoiceOver) audio", target: self, action: #selector(screenReaderToggled)) screenReaderCheckbox.state = excludeScreenReader ? .on : .off screenReaderCheckbox.toolTip = "Keep your VoiceOver speech out of the shared audio." self.screenReaderCheckbox = screenReaderCheckbox let cancelButton = NSButton(title: "Cancel", target: self, action: #selector(cancelClicked)) cancelButton.bezelStyle = .rounded cancelButton.keyEquivalent = "\u{1b}" // Esc let shareButton = NSButton(title: "Share", target: self, action: #selector(shareClicked)) shareButton.bezelStyle = .rounded shareButton.keyEquivalent = "\r" let buttonRow = NSStackView(views: [NSView(), cancelButton, shareButton]) buttonRow.orientation = .horizontal buttonRow.spacing = 8 let stack = NSStackView(views: [titleLabel, modeControl, scroll, statusLabel, screenReaderCheckbox, buttonRow]) stack.orientation = .vertical stack.spacing = 8 stack.edgeInsets = NSEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) stack.translatesAutoresizingMaskIntoConstraints = false view.addSubview(stack) NSLayoutConstraint.activate([ stack.topAnchor.constraint(equalTo: view.topAnchor), stack.leadingAnchor.constraint(equalTo: view.leadingAnchor), stack.trailingAnchor.constraint(equalTo: view.trailingAnchor), stack.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) updateEnabledStates() } /// Reflect the current mode: the app table only matters for only/except; the screen-reader /// toggle is moot for `.only` (include-only already excludes the screen reader). private func updateEnabledStates() { let listActive = (mode != .everything) tableView.isEnabled = listActive tableView.alphaValue = listActive ? 1.0 : 0.45 screenReaderCheckbox?.isEnabled = (mode != .only) } private func loadApps() { Task { @MainActor in do { let content = try await SCShareableContent.current var seen = Set() var entries: [AppEntry] = [] for app in content.applications { let bid = app.bundleIdentifier guard !bid.isEmpty, !seen.contains(bid) else { continue } // Hide our own app (its audio is already excluded) and the screen reader // (handled by its own toggle). if bid == Bundle.main.bundleIdentifier { continue } if ScreenAudioCapture.screenReaderBundleIDs.contains(bid) { continue } seen.insert(bid) let name = app.applicationName.isEmpty ? bid : app.applicationName let icon = NSRunningApplication .runningApplications(withBundleIdentifier: bid).first?.icon entries.append(AppEntry(name: name, bundleID: bid, icon: icon)) } entries.sort { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } self.apps = entries self.statusLabel?.isHidden = true self.tableView.reloadData() } catch { self.statusLabel?.stringValue = "Screen Recording permission needed to list apps — " + "grant it in System Settings ▸ Privacy & Security, then reopen this." } } } // MARK: - Actions @objc private func modeChanged() { mode = Mode(rawValue: modeControl?.selectedSegment ?? 0) ?? .everything updateEnabledStates() } @objc private func screenReaderToggled() { excludeScreenReader = (screenReaderCheckbox?.state == .on) } @objc private func appCheckboxToggled(_ sender: NSButton) { let row = sender.tag guard row >= 0, row < apps.count else { return } let bid = apps[row].bundleID if sender.state == .on { checked.insert(bid) } else { checked.remove(bid) } } @objc private func shareClicked() { let scope: ScreenAudioScope switch mode { case .everything: scope = .entireDesktop case .only: scope = .onlyApps(Array(checked)) case .except: scope = .allExcept(Array(checked)) } dismiss(nil) onComplete?(ScreenAudioSelection(scope: scope, excludeScreenReader: excludeScreenReader)) } @objc private func cancelClicked() { dismiss(nil) onComplete?(nil) } // MARK: - NSTableViewDataSource / Delegate func numberOfRows(in tableView: NSTableView) -> Int { apps.count } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { let app = apps[row] let cell = AppCheckboxCell() cell.checkbox.title = app.name cell.checkbox.state = checked.contains(app.bundleID) ? .on : .off cell.checkbox.isEnabled = (mode != .everything) cell.checkbox.tag = row cell.checkbox.target = self cell.checkbox.action = #selector(appCheckboxToggled(_:)) cell.iconView.image = app.icon return cell } // Selecting a row shouldn't visually highlight — interaction is via the checkbox. func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool { false } } // One row: a leading app icon and a checkbox titled with the app name. private final class AppCheckboxCell: NSTableCellView { let iconView = NSImageView() let checkbox = NSButton(checkboxWithTitle: "", target: nil, action: nil) init() { super.init(frame: .zero) iconView.translatesAutoresizingMaskIntoConstraints = false checkbox.translatesAutoresizingMaskIntoConstraints = false addSubview(iconView) addSubview(checkbox) NSLayoutConstraint.activate([ iconView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4), iconView.centerYAnchor.constraint(equalTo: centerYAnchor), iconView.widthAnchor.constraint(equalToConstant: 16), iconView.heightAnchor.constraint(equalToConstant: 16), checkbox.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 6), checkbox.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -4), checkbox.centerYAnchor.constraint(equalTo: centerYAnchor), ]) } required init?(coder: NSCoder) { fatalError() } }