summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-07-18 15:44:33 +0300
committerPaul Buetow <paul@buetow.org>2025-07-18 15:44:33 +0300
commitaa84a890ba80ba70a6ac311786cb9d80ae3d9e42 (patch)
tree21375d95d8c3ca6738af5c0894aa76f9c7ade4b4
parent28e1bf32bf6f6a44d0e81c672af984059335a836 (diff)
feat: improve GUI usability with tooltips and reorganized layout
- 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 <noreply@anthropic.com>
-rw-r--r--go.mod3
-rw-r--r--go.sum2
-rw-r--r--internal/gui/app.go146
-rw-r--r--internal/gui/audio_player.go32
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,
)