Add EQ type selector, preset management, and AutoEQ browser UI
This commit is contained in:
+406
-15
@@ -5,10 +5,13 @@ import (
|
||||
|
||||
"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"
|
||||
@@ -17,43 +20,182 @@ import (
|
||||
type GraphicEqualizer struct {
|
||||
widget.BaseWidget
|
||||
|
||||
OnChanged func(band int, gain float64)
|
||||
OnPreampChanged func(gain float64)
|
||||
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
|
||||
container *fyne.Container
|
||||
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) *GraphicEqualizer {
|
||||
g := &GraphicEqualizer{}
|
||||
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", fyne.TextAlignTrailing),
|
||||
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))
|
||||
pre := newCaptionTextSizeLabel("Pre", fyne.TextAlignCenter)
|
||||
preampSlider := newEQSlider()
|
||||
preampSlider.SetValue(preamp)
|
||||
preampSlider.OnChanged = func(f float64) {
|
||||
|
||||
// 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)
|
||||
}
|
||||
preampSlider.UpdateToolTip()
|
||||
g.preampSlider.UpdateToolTip()
|
||||
if !g.isApplyingPreset && g.OnManualAdjustment != nil {
|
||||
g.OnManualAdjustment()
|
||||
}
|
||||
}
|
||||
preampSlider.UpdateToolTip()
|
||||
bandSlidersCtr.Add(container.NewBorder(nil, pre, nil, nil, preampSlider))
|
||||
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) {
|
||||
@@ -66,13 +208,17 @@ func (g *GraphicEqualizer) buildSliders(preamp float64, bands []string, bandGain
|
||||
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
|
||||
}
|
||||
g.container = container.NewStack(
|
||||
|
||||
return container.NewStack(
|
||||
container.NewBorder(nil, widget.NewLabel(""), nil, nil,
|
||||
container.NewBorder(nil, nil, util.NewHSpace(5), util.NewHSpace(5),
|
||||
container.NewVBox(
|
||||
@@ -86,6 +232,251 @@ func (g *GraphicEqualizer) buildSliders(preamp float64, bands []string, bandGain
|
||||
)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
Reference in New Issue
Block a user