From aa84a890ba80ba70a6ac311786cb9d80ae3d9e42 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 18 Jul 2025 15:44:33 +0300 Subject: feat: improve GUI usability with tooltips and reorganized layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove button captions in favor of icon-only buttons - Add comprehensive keyboard shortcuts dialog (File->Hotkeys) - Move toolbar with action buttons to top of window - Add hover tooltips to all buttons showing function and shortcut - Change export hotkey from 'E' to 'e' - Replace audio regenerate icon with more appropriate record icon - Add fyne-tooltip dependency for tooltip functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- go.mod | 3 +- go.sum | 2 + internal/gui/app.go | 146 +++++++++++++++++++++++++++---------------- internal/gui/audio_player.go | 32 +++++----- 4 files changed, 111 insertions(+), 72 deletions(-) diff --git a/go.mod b/go.mod index dc47cf2..7e1ed77 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.24.4 require ( fyne.io/fyne/v2 v2.6.1 + github.com/dweymouth/fyne-tooltip v0.3.3 + github.com/mattn/go-sqlite3 v1.14.28 github.com/sashabaranov/go-openai v1.40.5 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.1 @@ -30,7 +32,6 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 // indirect github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect - github.com/mattn/go-sqlite3 v1.14.28 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect diff --git a/go.sum b/go.sum index 9316d14..cffb007 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dweymouth/fyne-tooltip v0.3.3 h1:blgS9trHPIe6t4cbXv912Ky2VSwL3q/+BqgRjibzH1o= +github.com/dweymouth/fyne-tooltip v0.3.3/go.mod h1:jXYbY561DTIXXqkauzltI3o/hsVaQd2KY8Ry2FXy7Xk= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= diff --git a/internal/gui/app.go b/internal/gui/app.go index 5f3699c..7e2e65f 100644 --- a/internal/gui/app.go +++ b/internal/gui/app.go @@ -17,6 +17,8 @@ import ( "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" "github.com/sashabaranov/go-openai" + fynetooltip "github.com/dweymouth/fyne-tooltip" + ttwidget "github.com/dweymouth/fyne-tooltip/widget" "codeberg.org/snonux/totalrecall/internal" "codeberg.org/snonux/totalrecall/internal/anki" @@ -31,7 +33,7 @@ type Application struct { // UI elements wordInput *widget.Entry - submitButton *widget.Button + submitButton *ttwidget.Button imageDisplay *ImageDisplay audioPlayer *AudioPlayer translationEntry *widget.Entry @@ -41,16 +43,16 @@ type Application struct { phoneticDisplay *widget.Label // Navigation buttons - prevWordBtn *widget.Button - nextWordBtn *widget.Button + prevWordBtn *ttwidget.Button + nextWordBtn *ttwidget.Button // Action buttons - keepButton *widget.Button - regenerateImageBtn *widget.Button - regenerateRandomImageBtn *widget.Button - regenerateAudioBtn *widget.Button - regenerateAllBtn *widget.Button - deleteButton *widget.Button + keepButton *ttwidget.Button + regenerateImageBtn *ttwidget.Button + regenerateRandomImageBtn *ttwidget.Button + regenerateAudioBtn *ttwidget.Button + regenerateAllBtn *ttwidget.Button + deleteButton *ttwidget.Button // State management currentWord string @@ -193,17 +195,17 @@ func (a *Application) setupUI() { } // Create navigation buttons with tooltips - a.submitButton = widget.NewButtonWithIcon("", theme.ConfirmIcon(), a.onSubmit) - submitLabel := widget.NewLabelWithStyle("Generate (g)", fyne.TextAlignCenter, fyne.TextStyle{Italic: true}) - submitBtn := container.NewVBox(a.submitButton, submitLabel) + a.submitButton = ttwidget.NewButton("", a.onSubmit) + a.submitButton.Icon = theme.ConfirmIcon() + a.submitButton.SetToolTip("Generate word (G)") - a.prevWordBtn = widget.NewButtonWithIcon("", theme.NavigateBackIcon(), a.onPrevWord) - prevLabel := widget.NewLabelWithStyle("Previous (←)", fyne.TextAlignCenter, fyne.TextStyle{Italic: true}) - prevBtn := container.NewVBox(a.prevWordBtn, prevLabel) + a.prevWordBtn = ttwidget.NewButton("", a.onPrevWord) + a.prevWordBtn.Icon = theme.NavigateBackIcon() + a.prevWordBtn.SetToolTip("Previous word (←)") - a.nextWordBtn = widget.NewButtonWithIcon("", theme.NavigateNextIcon(), a.onNextWord) - nextLabel := widget.NewLabelWithStyle("Next (→)", fyne.TextAlignCenter, fyne.TextStyle{Italic: true}) - nextBtn := container.NewVBox(a.nextWordBtn, nextLabel) + a.nextWordBtn = ttwidget.NewButton("", a.onNextWord) + a.nextWordBtn.Icon = theme.NavigateNextIcon() + a.nextWordBtn.SetToolTip("Next word (→)") // Create a grid layout for inputs inputGrid := container.New(layout.NewGridLayout(2), @@ -213,8 +215,8 @@ func (a *Application) setupUI() { inputSection := container.NewBorder( nil, nil, - prevBtn, - container.NewHBox(submitBtn, nextBtn), + a.prevWordBtn, + container.NewHBox(a.submitButton, a.nextWordBtn), inputGrid, ) @@ -278,44 +280,38 @@ func (a *Application) setupUI() { imageSection, ) - // Create action buttons with icons and tooltips - a.keepButton = widget.NewButtonWithIcon("", theme.DocumentCreateIcon(), a.onKeepAndContinue) - keepLabel := widget.NewLabelWithStyle("New Word (n)", fyne.TextAlignCenter, fyne.TextStyle{Italic: true}) - keepBtn := container.NewVBox(a.keepButton, keepLabel) + // Create action buttons with tooltips + a.keepButton = ttwidget.NewButtonWithIcon("", theme.DocumentCreateIcon(), a.onKeepAndContinue) + a.keepButton.SetToolTip("Keep card and new word (N)") - a.regenerateImageBtn = widget.NewButtonWithIcon("", theme.ViewRefreshIcon(), a.onRegenerateImage) - imageLabel := widget.NewLabelWithStyle("Regenerate Image (i)", fyne.TextAlignCenter, fyne.TextStyle{Italic: true}) - regenerateImageBtn := container.NewVBox(a.regenerateImageBtn, imageLabel) + a.regenerateImageBtn = ttwidget.NewButtonWithIcon("", theme.ViewRefreshIcon(), a.onRegenerateImage) + a.regenerateImageBtn.SetToolTip("Regenerate image (I)") - a.regenerateRandomImageBtn = widget.NewButtonWithIcon("", theme.MediaPhotoIcon(), a.onRegenerateRandomImage) - randomLabel := widget.NewLabelWithStyle("Random Image (m)", fyne.TextAlignCenter, fyne.TextStyle{Italic: true}) - regenerateRandomBtn := container.NewVBox(a.regenerateRandomImageBtn, randomLabel) + a.regenerateRandomImageBtn = ttwidget.NewButtonWithIcon("", theme.MediaPhotoIcon(), a.onRegenerateRandomImage) + a.regenerateRandomImageBtn.SetToolTip("Random image (M)") - a.regenerateAudioBtn = widget.NewButtonWithIcon("", theme.MediaPlayIcon(), a.onRegenerateAudio) - audioLabel := widget.NewLabelWithStyle("Regenerate Audio (a)", fyne.TextAlignCenter, fyne.TextStyle{Italic: true}) - regenerateAudioBtn := container.NewVBox(a.regenerateAudioBtn, audioLabel) + a.regenerateAudioBtn = ttwidget.NewButtonWithIcon("", theme.MediaRecordIcon(), a.onRegenerateAudio) + a.regenerateAudioBtn.SetToolTip("Regenerate audio (A)") - a.regenerateAllBtn = widget.NewButtonWithIcon("", theme.ViewFullScreenIcon(), a.onRegenerateAll) - allLabel := widget.NewLabelWithStyle("Regenerate All (r)", fyne.TextAlignCenter, fyne.TextStyle{Italic: true}) - regenerateAllBtn := container.NewVBox(a.regenerateAllBtn, allLabel) + a.regenerateAllBtn = ttwidget.NewButtonWithIcon("", theme.ViewFullScreenIcon(), a.onRegenerateAll) + a.regenerateAllBtn.SetToolTip("Regenerate all (R)") - a.deleteButton = widget.NewButtonWithIcon("", theme.DeleteIcon(), a.onDelete) + a.deleteButton = ttwidget.NewButtonWithIcon("", theme.DeleteIcon(), a.onDelete) a.deleteButton.Importance = widget.DangerImportance - deleteLabel := widget.NewLabelWithStyle("Delete (d)", fyne.TextAlignCenter, fyne.TextStyle{Italic: true}) - deleteBtn := container.NewVBox(a.deleteButton, deleteLabel) + a.deleteButton.SetToolTip("Delete word (D)") // Initially disable action buttons a.setActionButtonsEnabled(false) - actionSection := container.NewHBox( - keepBtn, - layout.NewSpacer(), - deleteBtn, + // Create toolbar with all action buttons aligned to the left + toolbar := container.NewHBox( + a.keepButton, + a.deleteButton, widget.NewSeparator(), - regenerateImageBtn, - regenerateRandomBtn, - regenerateAudioBtn, - regenerateAllBtn, + a.regenerateImageBtn, + a.regenerateRandomImageBtn, + a.regenerateAudioBtn, + a.regenerateAllBtn, ) // Create status section @@ -336,26 +332,28 @@ func (a *Application) setupUI() { fileMenu := fyne.NewMenu("File", fyne.NewMenuItem("Export to Anki... (E)", a.onExportToAnki), fyne.NewMenuItemSeparator(), + fyne.NewMenuItem("Hotkeys... (H)", a.onShowHotkeys), + fyne.NewMenuItemSeparator(), fyne.NewMenuItem("Quit", a.app.Quit), ) mainMenu := fyne.NewMainMenu(fileMenu) a.window.SetMainMenu(mainMenu) - // Combine all sections + // Combine all sections with toolbar at the top content := container.NewBorder( - inputSection, container.NewVBox( + toolbar, widget.NewSeparator(), - actionSection, - widget.NewSeparator(), - statusSection, + inputSection, ), + statusSection, nil, nil, displaySection, ) - a.window.SetContent(content) + // Add the tooltip layer to enable tooltips + a.window.SetContent(fynetooltip.AddWindowToolTipLayer(content, a.window.Canvas())) a.window.SetOnClosed(func() { a.cancel() a.queue.Stop() @@ -919,6 +917,45 @@ func (a *Application) onExportToAnki() { customDialog.Show() } +// onShowHotkeys displays a dialog with all available keyboard shortcuts +func (a *Application) onShowHotkeys() { + hotkeys := `## Navigation +**←** Previous word +**→** Next word +**Tab** Navigate fields +**Esc** Unfocus field + +## Word Processing +**G** Generate word +**N** New word +**D** Delete word + +## Regeneration +**I** Regenerate image +**M** Random image +**A** Regenerate audio +**R** Regenerate all +**P** Play audio + +## Export +**E** Export to Anki + +## Help +**H** Show hotkeys` + + content := widget.NewRichTextFromMarkdown(hotkeys) + content.Wrapping = fyne.TextWrapWord + + // Create a container with padding to prevent text cutoff + paddedContent := container.NewPadded(content) + + // Create a scrollable container for the content + scroll := container.NewScroll(paddedContent) + scroll.SetMinSize(fyne.NewSize(350, 450)) + + dialog.NewCustom("Keyboard Shortcuts", "Close", scroll, a.window).Show() +} + // Helper methods func (a *Application) setUIEnabled(enabled bool) { if enabled { @@ -1432,6 +1469,9 @@ func (a *Application) handleShortcutKey(key fyne.KeyName) { case fyne.KeyE: // Export to APKG a.onExportToAnki() + + case fyne.KeyH: // Show hotkeys + a.onShowHotkeys() } } diff --git a/internal/gui/audio_player.go b/internal/gui/audio_player.go index 63428bd..a94386c 100644 --- a/internal/gui/audio_player.go +++ b/internal/gui/audio_player.go @@ -11,6 +11,7 @@ import ( "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" + ttwidget "github.com/dweymouth/fyne-tooltip/widget" ) // AudioPlayer is a custom widget for playing audio files @@ -18,8 +19,8 @@ type AudioPlayer struct { widget.BaseWidget container *fyne.Container - playButton *widget.Button - stopButton *widget.Button + playButton *ttwidget.Button + stopButton *ttwidget.Button statusLabel *widget.Label audioFile string @@ -31,30 +32,25 @@ type AudioPlayer struct { func NewAudioPlayer() *AudioPlayer { p := &AudioPlayer{} - // Create controls - p.playButton = widget.NewButton("▶ Play (p)", p.onPlay) - p.stopButton = widget.NewButton("■ Stop", p.onStop) + // Create controls with tooltips + p.playButton = ttwidget.NewButton("", p.onPlay) + p.playButton.Icon = theme.MediaPlayIcon() + p.playButton.SetToolTip("Play audio (P)") + + p.stopButton = ttwidget.NewButton("", p.onStop) + p.stopButton.Icon = theme.MediaStopIcon() + p.stopButton.SetToolTip("Stop audio") + p.statusLabel = widget.NewLabel("No audio loaded") // Initially disable controls p.playButton.Disable() p.stopButton.Disable() - // Create containers with tooltips - playContainer := container.NewVBox( - p.playButton, - widget.NewLabelWithStyle("Play (p)", fyne.TextAlignCenter, fyne.TextStyle{Italic: true}), - ) - - stopContainer := container.NewVBox( - p.stopButton, - widget.NewLabelWithStyle("Stop", fyne.TextAlignCenter, fyne.TextStyle{Italic: true}), - ) - // Create main container p.container = container.NewHBox( - playContainer, - stopContainer, + p.playButton, + p.stopButton, layout.NewSpacer(), p.statusLabel, ) -- cgit v1.2.3