Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add floating palette popover controls

Add popover UI and shortcut recording for the floating piano, including show/focus controls and a configurable palette shortcut. Also extend releaseAllHeldNotes so floating tap-play notes are released correctly when focus capture or the floating palette exits.

authored by

Esteban Uribe and committed by
prompt.ac/@jeffrey
8f1ae496 2816b6f9

+154 -1
+13
slab/menuband/Sources/MenuBand/MenuBandController.swift
··· 919 919 heldKeyChannel.removeAll() 920 920 heldKeyDisplayNote.removeAll() 921 921 heldLock.unlock() 922 + let tapSnapshot = tapHeld 923 + let tapChanSnapshot = tapNoteChannel 924 + tapHeld.removeAll() 925 + tapNoteChannel.removeAll() 922 926 for (keyCode, note) in noteSnapshot { 923 927 let ch = chanSnapshot[keyCode] ?? 0 924 928 synth.noteOff(note, channel: ch) 925 929 midi.noteOff(note) 930 + } 931 + for note in tapSnapshot { 932 + let synthCh = tapChanSnapshot[note] ?? channel(for: note) 933 + let isDrum = note < UInt8(KeyboardIconRenderer.firstMidi) 934 + let midiCh: UInt8 = isDrum ? 9 : 0 935 + if !isDrum { 936 + synth.noteOff(note, channel: synthCh) 937 + } 938 + midi.noteOff(note, channel: midiCh) 926 939 } 927 940 midi.sendAllNotesOff() 928 941 synth.panic()
+141 -1
slab/menuband/Sources/MenuBand/MenuBandPopover.swift
··· 64 64 weak var popover: NSPopover? 65 65 var onFocusShortcutChange: ((MenuBandShortcut) -> Bool)? 66 66 var onFocusShortcutRecordingChanged: ((Bool) -> Void)? 67 + var onPlayPaletteToggle: (() -> Void)? 68 + var onPlayPaletteShortcutChange: ((MenuBandShortcut) -> Bool)? 69 + var onPlayPaletteShortcutRecordingChanged: ((Bool) -> Void)? 70 + var isPlayPaletteShown: (() -> Bool)? 67 71 68 72 private var inputSegmented: HoverSegmentedControl! // legacy reference; no longer added to stack 69 73 private var modeButtons: [NSButton] = [] // vertical stack: Mouse Only / Notepat.com / Ableton MIDI Keys ··· 71 75 private var focusShortcutStatusLabel: NSTextField! 72 76 private var focusShortcutRecorderMonitor: Any? 73 77 private var isRecordingFocusShortcut = false 78 + private var playPaletteToggleButton: NSButton! 79 + private var playPaletteShortcutButton: NSButton! 80 + private var playPaletteShortcutStatusLabel: NSTextField! 81 + private var playPaletteShortcutRecorderMonitor: Any? 82 + private var isRecordingPlayPaletteShortcut = false 74 83 private var midiSwitch: NSSwitch! 75 84 private var midiInlineLabel: NSTextField! 76 85 private var midiSelfTestLabel: NSTextField! // legacy — created but never added to stack ··· 99 108 100 109 deinit { 101 110 if let monitor = focusShortcutRecorderMonitor { 111 + NSEvent.removeMonitor(monitor) 112 + } 113 + if let monitor = playPaletteShortcutRecorderMonitor { 102 114 NSEvent.removeMonitor(monitor) 103 115 } 104 116 } ··· 323 335 equalToConstant: InstrumentListView.preferredWidth 324 336 ).isActive = true 325 337 338 + let playPaletteRow = NSStackView() 339 + playPaletteRow.orientation = .horizontal 340 + playPaletteRow.alignment = .centerY 341 + playPaletteRow.distribution = .fill 342 + playPaletteRow.spacing = 6 343 + playPaletteRow.translatesAutoresizingMaskIntoConstraints = false 344 + 345 + let playPaletteLabel = NSTextField(labelWithString: "Floating piano") 346 + playPaletteLabel.font = NSFont.systemFont(ofSize: 11) 347 + playPaletteLabel.textColor = .labelColor 348 + playPaletteLabel.lineBreakMode = .byTruncatingTail 349 + playPaletteLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) 350 + playPaletteRow.addArrangedSubview(playPaletteLabel) 351 + 352 + let playPaletteSpacer = NSView() 353 + playPaletteSpacer.setContentHuggingPriority(.defaultLow, for: .horizontal) 354 + playPaletteRow.addArrangedSubview(playPaletteSpacer) 355 + 356 + playPaletteToggleButton = NSButton( 357 + title: "Show", 358 + target: self, 359 + action: #selector(playPaletteToggleButtonClicked(_:)) 360 + ) 361 + playPaletteToggleButton.bezelStyle = .recessed 362 + playPaletteToggleButton.controlSize = .small 363 + playPaletteToggleButton.translatesAutoresizingMaskIntoConstraints = false 364 + playPaletteToggleButton.widthAnchor.constraint(equalToConstant: 48).isActive = true 365 + playPaletteRow.addArrangedSubview(playPaletteToggleButton) 366 + 367 + playPaletteShortcutButton = NSButton( 368 + title: MenuBandShortcutPreferences.playPaletteShortcut.displayString, 369 + target: self, 370 + action: #selector(playPaletteShortcutButtonClicked(_:)) 371 + ) 372 + playPaletteShortcutButton.bezelStyle = .recessed 373 + playPaletteShortcutButton.controlSize = .small 374 + playPaletteShortcutButton.translatesAutoresizingMaskIntoConstraints = false 375 + playPaletteShortcutButton.widthAnchor.constraint(equalToConstant: 88).isActive = true 376 + playPaletteRow.addArrangedSubview(playPaletteShortcutButton) 377 + 378 + playPaletteRow.widthAnchor.constraint( 379 + equalToConstant: InstrumentListView.preferredWidth 380 + ).isActive = true 381 + 382 + playPaletteShortcutStatusLabel = NSTextField(labelWithString: "") 383 + playPaletteShortcutStatusLabel.font = NSFont.systemFont(ofSize: 10) 384 + playPaletteShortcutStatusLabel.textColor = .secondaryLabelColor 385 + playPaletteShortcutStatusLabel.lineBreakMode = .byTruncatingTail 386 + playPaletteShortcutStatusLabel.widthAnchor.constraint( 387 + equalToConstant: InstrumentListView.preferredWidth 388 + ).isActive = true 389 + 326 390 // Vertical mode buttons — full labels fit without truncation, each 327 391 // button is the full content width with an SF Symbol leading the 328 392 // text so the mode is recognizable at a glance. ··· 671 735 stack.addArrangedSubview(shortcutLabel) 672 736 stack.addArrangedSubview(shortcuts) 673 737 stack.addArrangedSubview(focusShortcutStatusLabel) 738 + stack.addArrangedSubview(playPaletteRow) 739 + stack.addArrangedSubview(playPaletteShortcutStatusLabel) 674 740 675 741 // No divider above the about/brand block — the palette + Layout 676 742 // section above gives plenty of separation. Custom airspace 677 743 // before the about block. 678 - stack.setCustomSpacing(14, after: focusShortcutStatusLabel) 744 + stack.setCustomSpacing(14, after: playPaletteShortcutStatusLabel) 679 745 680 746 // About + Crash logs in a side-by-side row. About has low hugging 681 747 // so it expands when the crash column is hidden (no reports) — ··· 840 906 override func viewDidDisappear() { 841 907 super.viewDidDisappear() 842 908 stopFocusShortcutRecording(status: nil) 909 + stopPlayPaletteShortcutRecording(status: nil) 843 910 waveformView.isLive = false 844 911 } 845 912 ··· 939 1006 btn.state = (i == segIdx) ? .on : .off 940 1007 } 941 1008 updateFocusShortcutControls() 1009 + updatePlayPaletteShortcutControls() 942 1010 instrumentList.selectedProgram = n.melodicProgram 943 1011 applyAppearanceToVisualizer() 944 1012 updateInstrumentReadout() ··· 1010 1078 1011 1079 private func startFocusShortcutRecording() { 1012 1080 guard focusShortcutRecorderMonitor == nil else { return } 1081 + stopPlayPaletteShortcutRecording(status: nil) 1013 1082 isRecordingFocusShortcut = true 1014 1083 onFocusShortcutRecordingChanged?(true) 1015 1084 updateFocusShortcutControls(status: "Use ⌘, ⌃, or ⌥") ··· 1054 1123 status: saved ? "Saved \(shortcut.displayString)" : "Shortcut unavailable" 1055 1124 ) 1056 1125 } 1126 + private func updatePlayPaletteShortcutControls(status: String? = nil) { 1127 + guard playPaletteShortcutButton != nil else { return } 1128 + playPaletteToggleButton.title = (isPlayPaletteShown?() ?? false) ? "Focus" : "Show" 1129 + if isRecordingPlayPaletteShortcut { 1130 + playPaletteShortcutButton.title = "Press" 1131 + } else { 1132 + playPaletteShortcutButton.title = MenuBandShortcutPreferences.playPaletteShortcut.displayString 1133 + } 1134 + playPaletteShortcutStatusLabel.stringValue = status ?? "" 1135 + } 1136 + 1137 + private func startPlayPaletteShortcutRecording() { 1138 + guard playPaletteShortcutRecorderMonitor == nil else { return } 1139 + stopFocusShortcutRecording(status: nil) 1140 + isRecordingPlayPaletteShortcut = true 1141 + onPlayPaletteShortcutRecordingChanged?(true) 1142 + updatePlayPaletteShortcutControls(status: "Use ⌘, ⌃, or ⌥") 1143 + playPaletteShortcutRecorderMonitor = NSEvent.addLocalMonitorForEvents( 1144 + matching: [.keyDown] 1145 + ) { [weak self] event in 1146 + self?.handlePlayPaletteShortcutRecording(event) 1147 + return nil 1148 + } 1149 + } 1150 + 1151 + private func stopPlayPaletteShortcutRecording(status: String?) { 1152 + guard isRecordingPlayPaletteShortcut || playPaletteShortcutRecorderMonitor != nil else { return } 1153 + if let monitor = playPaletteShortcutRecorderMonitor { 1154 + NSEvent.removeMonitor(monitor) 1155 + playPaletteShortcutRecorderMonitor = nil 1156 + } 1157 + isRecordingPlayPaletteShortcut = false 1158 + onPlayPaletteShortcutRecordingChanged?(false) 1159 + updatePlayPaletteShortcutControls(status: status) 1160 + } 1161 + 1162 + private func handlePlayPaletteShortcutRecording(_ event: NSEvent) { 1163 + if event.keyCode == UInt16(kVK_Escape) { 1164 + stopPlayPaletteShortcutRecording(status: nil) 1165 + return 1166 + } 1167 + let shortcut = MenuBandShortcut( 1168 + keyCode: UInt32(event.keyCode), 1169 + modifiers: MenuBandShortcut.carbonModifiers(from: event.modifierFlags) 1170 + ) 1171 + guard shortcut.isValidForRecording else { 1172 + updatePlayPaletteShortcutControls(status: "Use ⌘, ⌃, or ⌥") 1173 + return 1174 + } 1175 + guard !shortcut.isReservedForTypeMode else { 1176 + updatePlayPaletteShortcutControls(status: "⌃⌥⌘P is reserved") 1177 + return 1178 + } 1179 + let saved = onPlayPaletteShortcutChange?(shortcut) ?? false 1180 + stopPlayPaletteShortcutRecording( 1181 + status: saved ? "Saved \(shortcut.displayString)" : "Shortcut unavailable" 1182 + ) 1183 + } 1057 1184 private func updateInstrumentReadout() { 1058 1185 guard let m = menuBand else { return } 1059 1186 let safe = max(0, min(127, Int(m.melodicProgram))) ··· 1451 1578 stopFocusShortcutRecording(status: nil) 1452 1579 } else { 1453 1580 startFocusShortcutRecording() 1581 + } 1582 + } 1583 + 1584 + @objc private func playPaletteToggleButtonClicked(_ sender: NSButton) { 1585 + onPlayPaletteToggle?() 1586 + updatePlayPaletteShortcutControls() 1587 + } 1588 + 1589 + @objc private func playPaletteShortcutButtonClicked(_ sender: NSButton) { 1590 + if isRecordingPlayPaletteShortcut { 1591 + stopPlayPaletteShortcutRecording(status: nil) 1592 + } else { 1593 + startPlayPaletteShortcutRecording() 1454 1594 } 1455 1595 } 1456 1596