Files
supersonic/ui/dialogs/graphicequalizer.go
T

521 lines
14 KiB
Go

package dialogs
import (
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
ttwidget "github.com/dweymouth/fyne-tooltip/widget"
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/ui/layouts"
myTheme "github.com/dweymouth/supersonic/ui/theme"
"github.com/dweymouth/supersonic/ui/util"
)
type GraphicEqualizer struct {
widget.BaseWidget
OnChanged func(band int, gain float64)
OnPreampChanged func(gain float64)
OnLoadAutoEQProfile func()
OnManualAdjustment func() // Called when user manually changes a slider
OnPresetSelected func(presetName string) // Called when user selects a preset
OnPresetDeleted func(presetName string) // Called when user deletes a preset
OnEQTypeChanged func(eqType string) // Called when EQ type is changed
bandSliders []*eqSlider
preampSlider *eqSlider
presetSelect *widget.Select
eqTypeSelect *widget.Select
autoEQBtn *widget.Button
profileLabel *widget.Label
container *fyne.Container
sliderArea *fyne.Container // Stores the slider area for dynamic rebuilding
topBar *fyne.Container // Stores the top bar
eqPresets []backend.EQPreset
presetManager *backend.EQPresetManager
parentWindow fyne.Window
isApplyingPreset bool // Flag to prevent clearing profile during preset application
currentEQType string // Current EQ type ("ISO10Band" or "ISO15Band")
}
func NewGraphicEqualizer(preamp float64, bandFreqs []string, bandGains []float64, eqType string, presetMgr *backend.EQPresetManager, parentWindow fyne.Window, activePresetName string) *GraphicEqualizer {
g := &GraphicEqualizer{
presetManager: presetMgr,
parentWindow: parentWindow,
currentEQType: eqType,
}
g.ExtendBaseWidget(g)
g.loadPresets()
g.buildSliders(preamp, bandFreqs, bandGains)
// Set the dropdown to the active preset if one exists
if activePresetName != "" {
g.setActivePreset(activePresetName)
}
return g
}
func (g *GraphicEqualizer) loadPresets() {
presets, err := g.presetManager.LoadPresets()
if err != nil {
// Fallback to empty list if load fails
g.eqPresets = []backend.EQPreset{}
return
}
g.eqPresets = presets
}
func (g *GraphicEqualizer) buildSliders(preamp float64, bands []string, bandGains []float64) {
// Build preset selector
g.updatePresetSelect()
// Build EQ type selector
if g.eqTypeSelect == nil {
g.eqTypeSelect = widget.NewSelect([]string{"ISO 15-Band", "ISO 10-Band"}, func(selected string) {
// Convert display name to type
newType := "ISO15Band"
if selected == "ISO 10-Band" {
newType = "ISO10Band"
}
if newType != g.currentEQType {
g.currentEQType = newType
if g.OnEQTypeChanged != nil {
g.OnEQTypeChanged(newType)
}
}
})
}
// Set current selection
if g.currentEQType == "ISO10Band" {
g.eqTypeSelect.SetSelected("ISO 10-Band")
} else {
g.eqTypeSelect.SetSelected("ISO 15-Band")
}
// Reset button
resetBtn := widget.NewButton(lang.L("Reset"), func() {
// Find and apply the "Flat" preset
for _, p := range g.eqPresets {
if p.Name == "Flat" {
g.applyPreset(p)
g.presetSelect.SetSelected(p.Name)
if g.OnPresetSelected != nil {
g.OnPresetSelected(p.Name)
}
break
}
}
})
// Save button
saveBtn := widget.NewButton(lang.L("Save"), func() {
g.showSavePresetDialog()
})
// Delete button
deleteBtn := widget.NewButton(lang.L("Delete"), func() {
g.showDeletePresetDialog()
})
// AutoEQ button
g.autoEQBtn = widget.NewButton(lang.L("AutoEQ"), func() {
if g.OnLoadAutoEQProfile != nil {
g.OnLoadAutoEQProfile()
}
})
// Profile label (hidden by default)
g.profileLabel = widget.NewLabel("")
g.profileLabel.Hide()
// Set minimum width for preset dropdown
g.presetSelect.Resize(fyne.NewSize(200, g.presetSelect.MinSize().Height))
// Top bar with controls - AutoEQ in main row for better discoverability
topBar := container.NewVBox(
// Main row: EQ type, preset selector, AutoEQ, and action buttons
container.NewHBox(
widget.NewLabel(lang.L("EQ Type:")),
g.eqTypeSelect,
widget.NewLabel(lang.L("EQ Preset:")),
g.presetSelect,
g.autoEQBtn,
layout.NewSpacer(),
saveBtn,
deleteBtn,
resetBtn,
),
// Second row: Profile label (shown only when AutoEQ profile is active)
g.profileLabel,
)
// Build slider area
g.sliderArea = g.buildSliderArea(preamp, bands, bandGains)
// Store topBar and create main container
g.topBar = topBar
g.container = container.NewBorder(g.topBar, nil, nil, nil, g.sliderArea)
}
func (g *GraphicEqualizer) buildSliderArea(preamp float64, bands []string, bandGains []float64) *fyne.Container {
// Range labels
rng := container.NewVBox(
newCaptionTextSizeLabel("+12", fyne.TextAlignTrailing),
layout.NewSpacer(),
newCaptionTextSizeLabel("0 dB", fyne.TextAlignTrailing),
layout.NewSpacer(),
newCaptionTextSizeLabel("-12", fyne.TextAlignTrailing),
)
g.bandSliders = make([]*eqSlider, len(bands))
bandSlidersCtr := container.New(layouts.NewGridLayoutWithColumnsAndPadding(len(bands)+2, -16))
// Preamp slider
pre := newCaptionTextSizeLabel(lang.L("EQ Preamp"), fyne.TextAlignCenter)
g.preampSlider = newEQSlider()
g.preampSlider.SetValue(preamp)
g.preampSlider.OnChanged = func(f float64) {
if g.OnPreampChanged != nil {
g.OnPreampChanged(f)
}
g.preampSlider.UpdateToolTip()
if !g.isApplyingPreset && g.OnManualAdjustment != nil {
g.OnManualAdjustment()
}
}
g.preampSlider.UpdateToolTip()
bandSlidersCtr.Add(container.NewBorder(nil, pre, nil, nil, g.preampSlider))
bandSlidersCtr.Add(container.NewBorder(nil, widget.NewLabel(""), nil, nil, rng))
// Band sliders
for i, band := range bands {
s := newEQSlider()
if i < len(bandGains) {
s.SetValue(bandGains[i])
s.UpdateToolTip()
}
_i := i
s.OnChanged = func(f float64) {
if g.OnChanged != nil {
g.OnChanged(_i, f)
}
g.bandSliders[_i].UpdateToolTip()
if !g.isApplyingPreset && g.OnManualAdjustment != nil {
g.OnManualAdjustment()
}
}
l := newCaptionTextSizeLabel(band, fyne.TextAlignCenter)
c := container.NewBorder(nil, l, nil, nil, s)
bandSlidersCtr.Add(c)
g.bandSliders[i] = s
}
return container.NewStack(
container.NewBorder(nil, widget.NewLabel(""), nil, nil,
container.NewBorder(nil, nil, util.NewHSpace(5), util.NewHSpace(5),
container.NewVBox(
layout.NewSpacer(),
myTheme.NewThemedRectangle(theme.ColorNameInputBackground),
layout.NewSpacer(),
),
),
),
bandSlidersCtr,
)
}
// RebuildForEQType rebuilds the sliders for a new EQ type
func (g *GraphicEqualizer) RebuildForEQType(eqType string, bandGains []float64) {
// Determine band frequencies for the new type
var bands []string
if eqType == "ISO10Band" {
bands = []string{"31", "62", "125", "250", "500", "1k", "2k", "4k", "8k", "16k"}
} else {
bands = []string{"25", "40", "63", "100", "160", "250", "400", "630", "1k", "1.6k", "2.5k", "4k", "6.3k", "10k", "16k"}
}
// Get current preamp value
currentPreamp := 0.0
if g.preampSlider != nil {
currentPreamp = g.preampSlider.Value
}
// Rebuild the slider area
newSliderArea := g.buildSliderArea(currentPreamp, bands, bandGains)
// Replace the old slider area in the container
g.sliderArea = newSliderArea
g.container.Objects = []fyne.CanvasObject{g.topBar, g.sliderArea}
g.container.Refresh()
}
func (g *GraphicEqualizer) updatePresetSelect() {
presetNames := make([]string, len(g.eqPresets))
for i, p := range g.eqPresets {
displayName := p.Name
if !p.IsBuiltin {
displayName = p.Name + " *" // Mark custom presets with asterisk
}
presetNames[i] = displayName
}
if g.presetSelect == nil {
g.presetSelect = widget.NewSelect(presetNames, func(selected string) {
// Remove asterisk marker if present
cleanName := selected
if len(selected) > 2 && selected[len(selected)-2:] == " *" {
cleanName = selected[:len(selected)-2]
}
for _, p := range g.eqPresets {
if p.Name == cleanName {
g.applyPreset(p)
if g.OnPresetSelected != nil {
g.OnPresetSelected(cleanName)
}
break
}
}
})
g.presetSelect.PlaceHolder = lang.L("EQ Preset")
} else {
g.presetSelect.Options = presetNames
g.presetSelect.Refresh()
}
}
// setActivePreset sets the dropdown selection to match the given preset name
func (g *GraphicEqualizer) setActivePreset(presetName string) {
if presetName == "" {
return
}
// Find the preset and determine display name (with asterisk for custom)
for _, p := range g.eqPresets {
if p.Name == presetName {
displayName := p.Name
if !p.IsBuiltin {
displayName = p.Name + " *"
}
g.presetSelect.SetSelected(displayName)
return
}
}
}
func (g *GraphicEqualizer) applyPreset(preset backend.EQPreset) {
g.isApplyingPreset = true
defer func() { g.isApplyingPreset = false }()
// If preset type differs from current type, switch type first
if preset.Type != "" && preset.Type != g.currentEQType {
g.currentEQType = preset.Type
// Update the type selector UI
if preset.Type == "ISO10Band" {
g.eqTypeSelect.SetSelected("ISO 10-Band")
} else {
g.eqTypeSelect.SetSelected("ISO 15-Band")
}
// Notify about type change
if g.OnEQTypeChanged != nil {
g.OnEQTypeChanged(preset.Type)
}
}
// Apply preamp
g.preampSlider.SetValue(preset.Preamp)
g.preampSlider.UpdateToolTip()
if g.OnPreampChanged != nil {
g.OnPreampChanged(preset.Preamp)
}
// Apply band gains
for i, gain := range preset.Bands {
if i < len(g.bandSliders) {
g.bandSliders[i].SetValue(gain)
g.bandSliders[i].UpdateToolTip()
if g.OnChanged != nil {
g.OnChanged(i, gain)
}
}
}
}
func (g *GraphicEqualizer) getCurrentSettings() backend.EQPreset {
bands := make([]float64, len(g.bandSliders))
for i, slider := range g.bandSliders {
bands[i] = slider.Value
}
return backend.EQPreset{
Type: g.currentEQType,
Preamp: g.preampSlider.Value,
Bands: bands,
}
}
func (g *GraphicEqualizer) showSavePresetDialog() {
nameEntry := widget.NewEntry()
nameEntry.SetPlaceHolder(lang.L("Preset name"))
formDialog := dialog.NewForm(
lang.L("Save Preset"),
lang.L("Save"),
lang.L("Cancel"),
[]*widget.FormItem{
widget.NewFormItem(lang.L("Name"), nameEntry),
},
func(confirmed bool) {
if !confirmed || nameEntry.Text == "" {
return
}
preset := g.getCurrentSettings()
preset.Name = nameEntry.Text
preset.IsBuiltin = false
if err := g.presetManager.SavePreset(preset); err != nil {
dialog.ShowError(err, g.parentWindow)
return
}
// Reload presets and update UI
g.loadPresets()
g.updatePresetSelect()
// Select the newly saved preset
g.presetSelect.SetSelected(preset.Name + " *")
if g.OnPresetSelected != nil {
g.OnPresetSelected(preset.Name)
}
},
g.parentWindow,
)
formDialog.Resize(fyne.NewSize(400, 150))
formDialog.Show()
}
func (g *GraphicEqualizer) showDeletePresetDialog() {
selected := g.presetSelect.Selected
if selected == "" {
dialog.ShowInformation(lang.L("No Preset Selected"), lang.L("Please select a preset to delete"), g.parentWindow)
return
}
// Remove asterisk marker if present
cleanName := selected
if len(selected) > 2 && selected[len(selected)-2:] == " *" {
cleanName = selected[:len(selected)-2]
}
// Find the preset
var presetToDelete *backend.EQPreset
for i, p := range g.eqPresets {
if p.Name == cleanName {
presetToDelete = &g.eqPresets[i]
break
}
}
if presetToDelete == nil || presetToDelete.IsBuiltin {
dialog.ShowInformation(lang.L("Cannot Delete"), lang.L("Cannot delete builtin presets"), g.parentWindow)
return
}
dialog.ShowConfirm(
lang.L("Delete Preset"),
fmt.Sprintf(lang.L("Delete preset '%s'?"), cleanName),
func(confirmed bool) {
if !confirmed {
return
}
if err := g.presetManager.DeletePreset(cleanName); err != nil {
dialog.ShowError(err, g.parentWindow)
return
}
// Notify about deletion
if g.OnPresetDeleted != nil {
g.OnPresetDeleted(cleanName)
}
// Reload presets and update UI
g.loadPresets()
g.updatePresetSelect()
g.presetSelect.ClearSelected()
},
g.parentWindow,
)
}
// SetProfileLabel displays the name of the applied AutoEQ profile
func (g *GraphicEqualizer) SetProfileLabel(profileName string) {
if profileName == "" {
g.profileLabel.SetText("")
g.profileLabel.Hide()
} else {
g.profileLabel.SetText(fmt.Sprintf("%s: %s", lang.L("Profile"), profileName))
g.profileLabel.Show()
}
}
// ClearProfileLabel hides the profile label (called on manual adjustment)
func (g *GraphicEqualizer) ClearProfileLabel() {
g.SetProfileLabel("")
}
// ClearPresetSelection clears the preset dropdown selection
func (g *GraphicEqualizer) ClearPresetSelection() {
g.presetSelect.ClearSelected()
}
func newCaptionTextSizeLabel(text string, alignment fyne.TextAlign) *widget.RichText {
l := widget.NewRichTextWithText(text)
ts := l.Segments[0].(*widget.TextSegment)
ts.Style.SizeName = theme.SizeNameCaptionText
ts.Style.Alignment = alignment
return l
}
func (g *GraphicEqualizer) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(g.container)
}
type eqSlider struct {
ttwidget.Slider
}
func newEQSlider() *eqSlider {
s := &eqSlider{
Slider: ttwidget.Slider{
Slider: widget.Slider{
Orientation: widget.Vertical,
Min: -12,
Max: 12,
Step: 0.1,
},
},
}
s.UpdateToolTip()
s.ExtendBaseWidget(s)
return s
}
func (s *eqSlider) UpdateToolTip() {
s.SetToolTip(fmt.Sprintf("%0.1f dB", s.Value))
}
// We implement our own double tapping so that the Tapped behavior
// can be triggered instantly.
func (s *eqSlider) DoubleTapped(e *fyne.PointEvent) {
s.SetValue(0)
}