Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

Polish floating palette presentation and dragging

authored by

Esteban Uribe and committed by
prompt.ac/@jeffrey
b35b575a 94deae9d

+163 -2
+163 -2
slab/menuband/Sources/MenuBand/FloatingPlayPalette.swift
··· 276 276 private weak var menuBand: MenuBandController? 277 277 private let waveformView = WaveformView() 278 278 private let waveformBezel = NSView() 279 + private let heldNotesStack = NSStackView() 280 + private let heldNotesContainer = NSView() 281 + private let instrumentReadout = NSTextField(labelWithString: "") 282 + private let instrumentTitleRow: NSStackView 279 283 private let pianoView: FloatingPianoView 280 284 private let dragHandle = FloatingPaletteDragHandleView() 281 285 private let closeButton = NSButton() ··· 290 294 private let closeSize: CGFloat = 18 291 295 private let hintHeight: CGFloat = 42 292 296 private var waveformHeightConstraint: NSLayoutConstraint? 297 + private var paletteDragStartMouse: NSPoint? 298 + private var paletteDragStartWindowOrigin: NSPoint? 299 + private var paletteDragActive = false 293 300 294 301 init(menuBand: MenuBandController) { 295 302 self.menuBand = menuBand 303 + let titleLeftSpacer = NSView() 304 + let titleRightSpacer = NSView() 305 + titleLeftSpacer.setContentHuggingPriority(.defaultLow, for: .horizontal) 306 + titleRightSpacer.setContentHuggingPriority(.defaultLow, for: .horizontal) 307 + self.instrumentTitleRow = NSStackView(views: [titleLeftSpacer, instrumentReadout, titleRightSpacer]) 296 308 self.pianoView = FloatingPianoView(menuBand: menuBand, pianoScale: pianoScale) 297 309 super.init(frame: NSRect(origin: .zero, size: .zero)) 298 310 wantsLayer = true 311 + let dragRecognizer = NSPanGestureRecognizer(target: self, action: #selector(handlePalettePan(_:))) 312 + addGestureRecognizer(dragRecognizer) 299 313 300 314 waveformView.menuBand = menuBand 301 315 waveformView.translatesAutoresizingMaskIntoConstraints = false ··· 304 318 waveformBezel.layer?.backgroundColor = NSColor(white: 0.06, alpha: 1.0).cgColor 305 319 waveformBezel.layer?.borderWidth = 1 306 320 waveformBezel.translatesAutoresizingMaskIntoConstraints = false 321 + heldNotesStack.orientation = .horizontal 322 + heldNotesStack.alignment = .centerY 323 + heldNotesStack.spacing = 4 324 + heldNotesStack.translatesAutoresizingMaskIntoConstraints = false 325 + heldNotesContainer.translatesAutoresizingMaskIntoConstraints = false 326 + heldNotesContainer.addSubview(heldNotesStack) 327 + instrumentReadout.lineBreakMode = .byTruncatingTail 328 + instrumentReadout.alignment = .center 329 + instrumentReadout.setContentHuggingPriority(.defaultHigh, for: .horizontal) 330 + instrumentReadout.setContentCompressionResistancePriority(.required, for: .horizontal) 331 + instrumentTitleRow.orientation = .horizontal 332 + instrumentTitleRow.alignment = .centerY 333 + instrumentTitleRow.distribution = .fill 334 + instrumentTitleRow.spacing = 0 335 + instrumentTitleRow.translatesAutoresizingMaskIntoConstraints = false 307 336 pianoView.translatesAutoresizingMaskIntoConstraints = false 308 337 dragHandle.translatesAutoresizingMaskIntoConstraints = false 309 338 closeButton.translatesAutoresizingMaskIntoConstraints = false 310 339 shortcutHintLabel.translatesAutoresizingMaskIntoConstraints = false 311 340 waveformBezel.addSubview(waveformView) 341 + addSubview(heldNotesContainer) 312 342 addSubview(waveformBezel) 343 + addSubview(instrumentTitleRow) 313 344 addSubview(pianoView) 314 345 shortcutHintLabel.font = NSFont.systemFont(ofSize: 10) 315 346 shortcutHintLabel.textColor = .secondaryLabelColor ··· 337 368 ) 338 369 self.waveformHeightConstraint = waveformHeightConstraint 339 370 let bezelInset: CGFloat = 5 371 + let titleSpacers = instrumentTitleRow.arrangedSubviews 340 372 341 373 NSLayoutConstraint.activate([ 342 374 widthAnchor.constraint(equalToConstant: keyboardSize.width + inset * 2), ··· 351 383 dragHandle.centerYAnchor.constraint(equalTo: closeButton.centerYAnchor), 352 384 dragHandle.heightAnchor.constraint(equalToConstant: closeSize), 353 385 354 - waveformBezel.topAnchor.constraint(equalTo: closeButton.bottomAnchor, constant: gap), 386 + heldNotesStack.centerXAnchor.constraint(equalTo: heldNotesContainer.centerXAnchor), 387 + heldNotesStack.centerYAnchor.constraint(equalTo: heldNotesContainer.centerYAnchor), 388 + heldNotesContainer.topAnchor.constraint(equalTo: closeButton.bottomAnchor, constant: gap), 389 + heldNotesContainer.leadingAnchor.constraint(equalTo: leadingAnchor, constant: inset), 390 + heldNotesContainer.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -inset), 391 + heldNotesContainer.heightAnchor.constraint(equalToConstant: 22), 392 + 393 + waveformBezel.topAnchor.constraint(equalTo: heldNotesContainer.bottomAnchor, constant: gap), 355 394 waveformBezel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: inset), 356 395 waveformBezel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -inset), 357 396 waveformView.leadingAnchor.constraint(equalTo: waveformBezel.leadingAnchor, constant: bezelInset), ··· 360 399 waveformView.bottomAnchor.constraint(equalTo: waveformBezel.bottomAnchor, constant: -bezelInset), 361 400 waveformHeightConstraint, 362 401 363 - pianoView.topAnchor.constraint(equalTo: waveformBezel.bottomAnchor, constant: gap), 402 + instrumentTitleRow.topAnchor.constraint(equalTo: waveformBezel.bottomAnchor, constant: gap), 403 + instrumentTitleRow.leadingAnchor.constraint(equalTo: leadingAnchor, constant: inset), 404 + instrumentTitleRow.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -inset), 405 + 406 + pianoView.topAnchor.constraint(equalTo: instrumentTitleRow.bottomAnchor, constant: gap), 364 407 pianoView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: inset), 365 408 pianoView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -inset), 366 409 ··· 370 413 shortcutHintLabel.heightAnchor.constraint(equalToConstant: hintHeight), 371 414 shortcutHintLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -inset) 372 415 ]) 416 + if titleSpacers.count == 3 { 417 + titleSpacers[0].widthAnchor.constraint(equalTo: titleSpacers[2].widthAnchor).isActive = true 418 + } 373 419 } 374 420 375 421 @available(*, unavailable) ··· 408 454 pianoView.refreshLayout() 409 455 layoutSubtreeIfNeeded() 410 456 applyAppearanceToVisualizer() 457 + refreshHeldNotes() 458 + updateInstrumentReadout() 411 459 applyWaveformTint() 412 460 updateWaveformLiveState(isPresented: window?.isVisible == true) 413 461 needsDisplay = true ··· 467 515 keyboard.height * 2.0 468 516 } 469 517 518 + private func refreshHeldNotes() { 519 + guard let menuBand else { return } 520 + for view in heldNotesStack.arrangedSubviews { 521 + heldNotesStack.removeArrangedSubview(view) 522 + view.removeFromSuperview() 523 + } 524 + let names = menuBand.heldNoteNames() 525 + let safe = max(0, min(127, Int(menuBand.effectiveMelodicProgram))) 526 + let familyColor = menuBand.midiMode 527 + ? NSColor.controlAccentColor 528 + : InstrumentListView.colorForProgram(safe) 529 + for name in names { 530 + heldNotesStack.addArrangedSubview(makeHeldNoteBox(name: name, color: familyColor)) 531 + } 532 + } 533 + 534 + private func makeHeldNoteBox(name: String, color: NSColor) -> NSView { 535 + let box = NSView() 536 + box.wantsLayer = true 537 + box.layer?.cornerRadius = 4 538 + box.layer?.backgroundColor = color.withAlphaComponent(0.85).cgColor 539 + box.translatesAutoresizingMaskIntoConstraints = false 540 + let label = NSTextField(labelWithString: name) 541 + label.font = NSFont.monospacedSystemFont(ofSize: 10, weight: .heavy) 542 + label.textColor = .black 543 + label.drawsBackground = false 544 + label.translatesAutoresizingMaskIntoConstraints = false 545 + box.addSubview(label) 546 + NSLayoutConstraint.activate([ 547 + label.leadingAnchor.constraint(equalTo: box.leadingAnchor, constant: 5), 548 + label.trailingAnchor.constraint(equalTo: box.trailingAnchor, constant: -5), 549 + label.topAnchor.constraint(equalTo: box.topAnchor, constant: 1), 550 + label.bottomAnchor.constraint(equalTo: box.bottomAnchor, constant: -1), 551 + ]) 552 + return box 553 + } 554 + 555 + private func updateInstrumentReadout() { 556 + guard let menuBand else { return } 557 + let safe = max(0, min(127, Int(menuBand.effectiveMelodicProgram))) 558 + let title = GeneralMIDI.programNames[safe] 559 + let familyColor = InstrumentListView.colorForProgram(safe) 560 + let isDark = effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua 561 + let textColor: NSColor = isDark ? .white : .black 562 + let shadow = NSShadow() 563 + shadow.shadowColor = (familyColor.highlight(withLevel: isDark ? 0.3 : 0.7) ?? familyColor) 564 + shadow.shadowOffset = NSSize(width: 1, height: -1) 565 + shadow.shadowBlurRadius = 0 566 + let titleFont: NSFont = { 567 + if let desc = AppDelegate.ywftBoldDescriptor, 568 + let font = NSFont(descriptor: desc, size: 18), 569 + font.familyName == "YWFT Processing" { 570 + return font 571 + } 572 + NSLog("MenuBand: YWFT bold descriptor unavailable; floating title falling back to system font") 573 + return NSFont.systemFont(ofSize: 18, weight: .black) 574 + }() 575 + instrumentReadout.attributedStringValue = NSAttributedString( 576 + string: title, 577 + attributes: [ 578 + .font: titleFont, 579 + .foregroundColor: textColor, 580 + .shadow: shadow, 581 + ] 582 + ) 583 + } 584 + 470 585 private func updateShortcutHint() { 471 586 let floatingShortcut = MenuBandShortcutPreferences.playPaletteShortcut.displayString 472 587 let focusShortcut = MenuBandShortcutPreferences.focusShortcut.displayString ··· 480 595 481 596 @objc private func closeClicked(_ sender: NSButton) { 482 597 onClose?() 598 + } 599 + 600 + @objc private func handlePalettePan(_ recognizer: NSPanGestureRecognizer) { 601 + let location = recognizer.location(in: self) 602 + switch recognizer.state { 603 + case .began: 604 + guard shouldBeginPaletteDrag(at: location) else { 605 + paletteDragActive = false 606 + return 607 + } 608 + paletteDragStartMouse = NSEvent.mouseLocation 609 + paletteDragStartWindowOrigin = window?.frame.origin 610 + paletteDragActive = true 611 + NSCursor.closedHand.set() 612 + case .changed: 613 + guard paletteDragActive, 614 + let window, 615 + let startMouse = paletteDragStartMouse, 616 + let startOrigin = paletteDragStartWindowOrigin else { return } 617 + let mouse = NSEvent.mouseLocation 618 + let nextOrigin = NSPoint( 619 + x: startOrigin.x + mouse.x - startMouse.x, 620 + y: startOrigin.y + mouse.y - startMouse.y 621 + ) 622 + NSAnimationContext.runAnimationGroup { context in 623 + context.duration = 0 624 + context.allowsImplicitAnimation = false 625 + window.setFrameOrigin(nextOrigin) 626 + } 627 + case .ended, .cancelled, .failed: 628 + if paletteDragActive { 629 + NSCursor.openHand.set() 630 + } 631 + paletteDragStartMouse = nil 632 + paletteDragStartWindowOrigin = nil 633 + paletteDragActive = false 634 + default: 635 + break 636 + } 637 + } 638 + 639 + private func shouldBeginPaletteDrag(at location: NSPoint) -> Bool { 640 + if closeButton.frame.contains(location) || pianoView.frame.contains(location) { 641 + return false 642 + } 643 + return true 483 644 } 484 645 } 485 646