diff --git a/slab/menuband/Sources/MenuBand/AboutWindow.swift b/slab/menuband/Sources/MenuBand/AboutWindow.swift index cd0a2571fb..0220a2049f 100644 --- a/slab/menuband/Sources/MenuBand/AboutWindow.swift +++ b/slab/menuband/Sources/MenuBand/AboutWindow.swift @@ -191,9 +191,19 @@ final class AboutWindowController: NSWindowController, NSWindowDelegate { private func buildContent() { guard let window = window else { return } - let content = NSView() - content.translatesAutoresizingMaskIntoConstraints = false - window.contentView = content + // REUSE the existing content view on a rebuild (language switch) rather + // than replacing window.contentView — replacing it re-composites the + // window's liquid-glass panel and causes a visible flash. Clearing and + // refilling the same view swaps the strings without touching the chrome. + let content: NSView + if let existing = window.contentView { + existing.subviews.forEach { $0.removeFromSuperview() } + content = existing + } else { + content = NSView() + content.translatesAutoresizingMaskIntoConstraints = false + window.contentView = content + } let stack = NSStackView() stack.orientation = .vertical @@ -465,11 +475,10 @@ final class AboutWindowController: NSWindowController, NSWindowDelegate { // never blocks the press / the audio. The string swap applies a beat // after the sound, keeping the tap feeling instant. DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - if let content = self.window?.contentView { - for sub in content.subviews { sub.removeFromSuperview() } - } - self.buildContent() + // buildContent() now reuses + refills the existing content view + // (no window.contentView swap), so the language change swaps strings + // without the liquid-glass panel flashing. + self?.buildContent() } } diff --git a/slab/menuband/Sources/MenuBand/JamWindow.swift b/slab/menuband/Sources/MenuBand/JamWindow.swift index 29f781ba1b..dd50bcc2c9 100644 --- a/slab/menuband/Sources/MenuBand/JamWindow.swift +++ b/slab/menuband/Sources/MenuBand/JamWindow.swift @@ -122,34 +122,39 @@ final class JamWindowController: NSWindowController, NSWindowDelegate { para.lineBreakMode = .byWordWrapping let nela = clubLink(L("popover.about.nela"), action: #selector(openNELA), - tooltip: "https://nelacomputer.club", para: para) + tooltip: "https://nelacomputer.club", para: para, + color: NSColor(red: 0.36, green: 0.80, blue: 0.92, alpha: 1)) // cyan stack.addArrangedSubview(nela) - stack.setCustomSpacing(4, after: nela) + stack.setCustomSpacing(12, after: nela) stack.addArrangedSubview(clubLink(L("popover.about.startaclub"), action: #selector(openStartAClub), tooltip: "https://startacomputer.club", - para: para)) + para: para, + color: NSColor(red: 1.0, green: 0.56, blue: 0.42, alpha: 1))) // coral } private func clubLink(_ text: String, action: Selector, - tooltip: String, para: NSParagraphStyle) -> NSButton { + tooltip: String, para: NSParagraphStyle, + color: NSColor) -> NSButton { let button = NSButton(title: "", target: self, action: action) button.attributedTitle = NSAttributedString( string: text, attributes: [ .font: NSFont.monospacedSystemFont(ofSize: 10, weight: .regular), - .foregroundColor: NSColor.secondaryLabelColor, + .foregroundColor: color, .underlineStyle: NSUnderlineStyle.single.rawValue, + .underlineColor: color.withAlphaComponent(0.6), .paragraphStyle: para, ]) button.bezelStyle = .regularSquare button.isBordered = false - button.cell?.wraps = true - button.cell?.lineBreakMode = .byWordWrapping + // Single line, sized to the text — a fixed 200pt width was wrapping + // longer links onto two lines (looked broken). + button.cell?.wraps = false + button.cell?.lineBreakMode = .byClipping if let cell = button.cell as? NSButtonCell { cell.imageScaling = .scaleNone } button.translatesAutoresizingMaskIntoConstraints = false button.toolTip = tooltip - button.widthAnchor.constraint(equalToConstant: 200).isActive = true return button } diff --git a/slab/menuband/Sources/MenuBand/MenuBandPopover.swift b/slab/menuband/Sources/MenuBand/MenuBandPopover.swift index 1bdfc9c6d9..ffae7b1dce 100644 --- a/slab/menuband/Sources/MenuBand/MenuBandPopover.swift +++ b/slab/menuband/Sources/MenuBand/MenuBandPopover.swift @@ -1764,10 +1764,17 @@ final class MenuBandPopoverViewController: NSViewController { /// the original neutral gray for any other caller. static func outlineFooterButton(_ button: NSButton, color: NSColor = NSColor(white: 0.5, alpha: 0.55)) { - button.wantsLayer = true - button.layer?.cornerRadius = 6 - button.layer?.borderWidth = 1 - button.layer?.borderColor = color.cgColor + // SOLID brand-colored bezel + white title — the old outline-only style + // was hard to read. Slightly darken the hue so white text always has + // strong contrast against the fill. + let solid = (color.withAlphaComponent(1.0).blended(withFraction: 0.16, of: .black)) ?? color + button.bezelColor = solid + button.contentTintColor = .white + if let attr = button.attributedTitle.mutableCopy() as? NSMutableAttributedString, attr.length > 0 { + attr.addAttribute(.foregroundColor, value: NSColor.white, + range: NSRange(location: 0, length: attr.length)) + button.attributedTitle = attr + } // Truncate a long localized title rather than letting the button's // intrinsic width grow the footer row (and with it the whole popover). // The popover width is locked to the instrument grid; footer labels diff --git a/slab/menuband/Sources/MenuBand/PianoWaveformWindow/ExpandedPianoWaveformView.swift b/slab/menuband/Sources/MenuBand/PianoWaveformWindow/ExpandedPianoWaveformView.swift index f1446385f9..79b450d3c7 100644 --- a/slab/menuband/Sources/MenuBand/PianoWaveformWindow/ExpandedPianoWaveformView.swift +++ b/slab/menuband/Sources/MenuBand/PianoWaveformWindow/ExpandedPianoWaveformView.swift @@ -359,7 +359,7 @@ final class ExpandedPianoWaveformView: NSView { let keyboardSize = self.keyboardSize() let widthConstraint = widthAnchor.constraint( - equalToConstant: max(keyboardSize.width + inset * 2, Self.expandedPanelWidth) + equalToConstant: stableKeyboardWidth() ) self.widthConstraint = widthConstraint let waveformHeightConstraint = waveformView.heightAnchor.constraint( @@ -668,7 +668,7 @@ final class ExpandedPianoWaveformView: NSView { updateHapticsControl() updateModeToggle() let keyboardSize = keyboardSize() - widthConstraint?.constant = max(keyboardSize.width + inset * 2, Self.expandedPanelWidth) + widthConstraint?.constant = stableKeyboardWidth() waveformHeightConstraint?.constant = waveformHeight(for: keyboardSize) pianoView.refreshLayout() layoutSubtreeIfNeeded() @@ -797,6 +797,21 @@ final class ExpandedPianoWaveformView: NSView { } } + /// A STABLE panel width so switching layouts (Notepat / Conventional / + /// Gamepad) never makes the overlay jump horizontally. Takes the widest + /// keyboard across keymaps and never shrinks below what's already shown — + /// narrower layouts simply re-center within the fixed width. + private func stableKeyboardWidth() -> CGFloat { + var widest: CGFloat = 0 + for km in [Keymap.notepat, .ableton] { + let w = KeyboardIconRenderer.withPianoWaveformKeyboard(keymap: km) { + KeyboardIconRenderer.pianoImageSize(layout: .tightActiveRange).width * pianoScale + } + widest = max(widest, w) + } + return max(widest + inset * 2, Self.expandedPanelWidth, widthConstraint?.constant ?? 0) + } + private func waveformHeight(for keyboard: NSSize) -> CGFloat { keyboard.height * 1.25 } diff --git a/slab/menuband/Sources/MenuBand/QwertyLayoutView.swift b/slab/menuband/Sources/MenuBand/QwertyLayoutView.swift index cd4740e4e0..fa368e9043 100644 --- a/slab/menuband/Sources/MenuBand/QwertyLayoutView.swift +++ b/slab/menuband/Sources/MenuBand/QwertyLayoutView.swift @@ -356,6 +356,12 @@ final class QwertyLayoutView: NSView { } else if mapped { NSColor(white: 0.92, alpha: 1.0).setFill() path.fill() + } else if !isOctaveKey { + // Ghost the inactive keys — a faint body (instead of a bare + // outline) so the mapped white/black caps stand out and the whole + // thing reads as a piano keyboard. + NSColor.labelColor.withAlphaComponent(0.05).setFill() + path.fill() } let stroke: NSColor if lit { @@ -365,7 +371,8 @@ final class QwertyLayoutView: NSView { } else if isDead { stroke = NSColor.labelColor.withAlphaComponent(0.18) } else { - stroke = NSColor.labelColor.withAlphaComponent(mapped ? 0.45 : 0.30) + // Ghost inactive keys: much fainter outline so mapped caps lead. + stroke = NSColor.labelColor.withAlphaComponent(mapped ? 0.45 : 0.13) } stroke.setStroke() path.lineWidth = 0.7 @@ -387,7 +394,9 @@ final class QwertyLayoutView: NSView { } else if mapped { textColor = NSColor(white: 0.10, alpha: 1.0) } else { - textColor = NSColor.labelColor.withAlphaComponent(0.55) + // Ghosted glyph on inactive keys so they recede behind the + // mapped piano caps. + textColor = NSColor.labelColor.withAlphaComponent(0.22) } let attrs: [NSAttributedString.Key: Any] = [ .font: NSFont.systemFont(ofSize: scaledLabelFontSize, weight: .heavy),