From edf85a41626f7caab6e44bcc82fbbaae3f7434b0 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 27 Jun 2024 17:44:41 -0700 Subject: [PATCH] add option to show album years in grid views --- backend/config.go | 4 ++ ui/browsing/albumspage.go | 8 +++- ui/browsing/artistspage.go | 6 ++- ui/browsing/browsingpane.go | 6 +++ ui/browsing/favoritespage.go | 8 ++++ ui/browsing/genrepage.go | 13 ++++-- ui/browsing/gridviewpage.go | 17 ++++++-- ui/browsing/router.go | 2 +- ui/controller/controller.go | 16 ++++---- ui/dialogs/settingsdialog.go | 10 +++++ ui/mainwindow.go | 1 + ui/theme/theme.go | 8 ++++ ui/widgets/gridview.go | 11 ++++- ui/widgets/gridviewitem.go | 33 +++++++++++---- ui/widgets/multihyperlink.go | 79 ++++++++++++++++++++++++++++-------- 15 files changed, 179 insertions(+), 43 deletions(-) diff --git a/backend/config.go b/backend/config.go index b232ef93..1cf2d143 100644 --- a/backend/config.go +++ b/backend/config.go @@ -59,6 +59,7 @@ type AlbumPageConfig struct { type AlbumsPageConfig struct { SortOrder string + ShowYears bool } type ArtistPageConfig struct { @@ -73,6 +74,7 @@ type ArtistsPageConfig struct { type FavoritesPageConfig struct { InitialView string TracklistColumns []string + ShowAlbumYears bool } type PlaylistPageConfig struct { @@ -167,6 +169,7 @@ func DefaultConfig(appVersionTag string) *Config { }, AlbumsPage: AlbumsPageConfig{ SortOrder: string("Recently Added"), + ShowYears: false, }, ArtistPage: ArtistPageConfig{ InitialView: "Discography", @@ -178,6 +181,7 @@ func DefaultConfig(appVersionTag string) *Config { FavoritesPage: FavoritesPageConfig{ TracklistColumns: []string{"Album", "Time", "Plays"}, InitialView: "Albums", + ShowAlbumYears: false, }, PlaylistPage: PlaylistPageConfig{ TracklistColumns: []string{"Album", "Time", "Plays"}, diff --git a/ui/browsing/albumspage.go b/ui/browsing/albumspage.go index c5f22520..5e3cfe60 100644 --- a/ui/browsing/albumspage.go +++ b/ui/browsing/albumspage.go @@ -72,6 +72,12 @@ func (a *albumsPageAdapter) SearchIter(query string, filter mediaprovider.AlbumF return widgets.NewGridViewAlbumIterator(a.mp.SearchAlbums(query, filter)) } -func (a *albumsPageAdapter) ConnectGridActions(gv *widgets.GridView) { +func (a *albumsPageAdapter) InitGrid(gv *widgets.GridView) { a.contr.ConnectAlbumGridActions(gv) + gv.ShowSuffix = a.cfg.ShowYears +} + +func (a *albumsPageAdapter) RefreshGrid(gv *widgets.GridView) { + gv.ShowSuffix = a.cfg.ShowYears + gv.Refresh() } diff --git a/ui/browsing/artistspage.go b/ui/browsing/artistspage.go index 0e29818f..6a713a25 100644 --- a/ui/browsing/artistspage.go +++ b/ui/browsing/artistspage.go @@ -68,7 +68,7 @@ func (a *artistsPageAdapter) SearchIter(query string, filter mediaprovider.Artis return widgets.NewGridViewArtistIterator(a.mp.SearchArtists(query, filter)) } -func (a *artistsPageAdapter) ConnectGridActions(gv *widgets.GridView) { +func (a *artistsPageAdapter) InitGrid(gv *widgets.GridView) { canShareArtists := false if r, canShare := a.mp.(mediaprovider.SupportsSharing); canShare { canShareArtists = r.CanShareArtists() @@ -76,3 +76,7 @@ func (a *artistsPageAdapter) ConnectGridActions(gv *widgets.GridView) { gv.DisableSharing = !canShareArtists a.contr.ConnectArtistGridActions(gv) } + +func (a *artistsPageAdapter) RefreshGrid(gv *widgets.GridView) { + gv.Refresh() +} diff --git a/ui/browsing/browsingpane.go b/ui/browsing/browsingpane.go index a5279dff..d2868d3b 100644 --- a/ui/browsing/browsingpane.go +++ b/ui/browsing/browsingpane.go @@ -195,6 +195,12 @@ func (b *BrowsingPane) ScrollDown() { } } +func (b *BrowsingPane) RefreshPage() { + if b.curPage != nil { + b.curPage.Refresh() + } +} + func (b *BrowsingPane) doSetPage(p Page) bool { if b.curPage != nil && b.curPage.Route() == p.Route() { return false diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index a550a3e0..bc1c1724 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -70,6 +70,7 @@ func NewFavoritesPage(cfg *backend.FavoritesPageConfig, pool *util.WidgetPool, c } else { a.albumGrid = widgets.NewGridView(iter, a.im, myTheme.AlbumIcon) } + a.albumGrid.ShowSuffix = cfg.ShowAlbumYears a.contr.ConnectAlbumGridActions(a.albumGrid) if cfg.InitialView == "Artists" { a.toggleBtns.SetActivatedButton(1) @@ -285,6 +286,13 @@ func (a *FavoritesPage) SelectAll() { } } +func (a *FavoritesPage) Refresh() { + if a.albumGrid != nil { + a.albumGrid.ShowSuffix = a.cfg.ShowAlbumYears + } + a.BaseWidget.Refresh() +} + func (a *FavoritesPage) tracklistOrNil() *widgets.Tracklist { if a.tracklistCtr != nil { return a.tracklistCtr.Objects[0].(*widgets.Tracklist) diff --git a/ui/browsing/genrepage.go b/ui/browsing/genrepage.go index 279d1e20..8e212121 100644 --- a/ui/browsing/genrepage.go +++ b/ui/browsing/genrepage.go @@ -14,6 +14,7 @@ import ( type genrePageAdapter struct { genre string + cfg *backend.AlbumsPageConfig contr *controller.Controller mp mediaprovider.MediaProvider pm *backend.PlaybackManager @@ -21,8 +22,8 @@ type genrePageAdapter struct { filterBtn *widgets.AlbumFilterButton } -func NewGenrePage(genre string, pool *util.WidgetPool, contr *controller.Controller, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager) Page { - adapter := &genrePageAdapter{genre: genre, contr: contr, mp: mp, pm: pm} +func NewGenrePage(genre string, cfg *backend.AlbumsPageConfig, pool *util.WidgetPool, contr *controller.Controller, pm *backend.PlaybackManager, mp mediaprovider.MediaProvider, im *backend.ImageManager) Page { + adapter := &genrePageAdapter{genre: genre, cfg: cfg, contr: contr, mp: mp, pm: pm} return NewGridViewPage(adapter, pool, mp, im) } @@ -68,6 +69,12 @@ func (a *genrePageAdapter) SearchIter(query string, filter mediaprovider.AlbumFi return widgets.NewGridViewAlbumIterator(a.mp.SearchAlbums(query, filter)) } -func (g *genrePageAdapter) ConnectGridActions(gv *widgets.GridView) { +func (g *genrePageAdapter) InitGrid(gv *widgets.GridView) { g.contr.ConnectAlbumGridActions(gv) + gv.ShowSuffix = g.cfg.ShowYears +} + +func (g *genrePageAdapter) RefreshGrid(gv *widgets.GridView) { + gv.ShowSuffix = g.cfg.ShowYears + gv.Refresh() } diff --git a/ui/browsing/gridviewpage.go b/ui/browsing/gridviewpage.go index 9b7580bd..99a31c2a 100644 --- a/ui/browsing/gridviewpage.go +++ b/ui/browsing/gridviewpage.go @@ -66,8 +66,13 @@ type GridViewPageAdapter[M, F any] interface { // Returns the iterator for the given search query and filter. SearchIter(query string, filter mediaprovider.MediaFilter[M, F]) widgets.GridViewIterator - // Function that connects the GridView callbacks to the appropriate action handlers. - ConnectGridActions(*widgets.GridView) + // Function that initialized the GridView with page-specific settings + // and connects the GridView callbacks to the appropriate action handlers. + InitGrid(*widgets.GridView) + + // Function called when settings may have changed and the grid needs + // reconfiguring with possible settings changes. + RefreshGrid(*widgets.GridView) } type SortableGridViewPageAdapter interface { @@ -124,7 +129,7 @@ func NewGridViewPage[M, F any]( gp.grid = widgets.NewGridView(iter, im, adapter.PlaceholderResource()) } gp.grid.DisableSharing = !canShare - adapter.ConnectGridActions(gp.grid) + adapter.InitGrid(gp.grid) gp.createSearchAndFilter() gp.createContainer() return gp @@ -206,6 +211,10 @@ func (g *GridViewPage[M, F]) OnSearched(query string) { g.searchText = query } +func (g *GridViewPage[M, F]) Refresh() { + g.adapter.RefreshGrid(g.grid) +} + func (g *GridViewPage[M, F]) doSearch(query string) { if g.searchText == "" { g.gridState = g.grid.SaveToState() @@ -294,7 +303,7 @@ func (s *savedGridViewPage[M, F]) Restore() Page { } else { gp.grid = widgets.NewGridViewFromState(state) } - gp.adapter.ConnectGridActions(gp.grid) + gp.adapter.InitGrid(gp.grid) gp.createSearchAndFilter() gp.createContainer() return gp diff --git a/ui/browsing/router.go b/ui/browsing/router.go index 92523094..cead13c6 100644 --- a/ui/browsing/router.go +++ b/ui/browsing/router.go @@ -44,7 +44,7 @@ func (r Router) CreatePage(rte controller.Route) Page { case controller.Favorites: return NewFavoritesPage(&r.App.Config.FavoritesPage, r.widgetPool, r.Controller, r.App.ServerManager.Server, r.App.PlaybackManager, r.App.ImageManager) case controller.Genre: - return NewGenrePage(rte.Arg, r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) + return NewGenrePage(rte.Arg, &r.App.Config.AlbumsPage, r.widgetPool, r.Controller, r.App.PlaybackManager, r.App.ServerManager.Server, r.App.ImageManager) case controller.Genres: return NewGenresPage(r.Controller, r.App.ServerManager.Server) case controller.NowPlaying: diff --git a/ui/controller/controller.go b/ui/controller/controller.go index cdfbbd7b..30a44a16 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -32,17 +32,16 @@ import ( type NavigationHandler func(Route) -type ReloadFunc func() - type CurPageFunc func() Route type Controller struct { - AppVersion string - MainWindow fyne.Window - App *backend.App - NavHandler NavigationHandler - CurPageFunc CurPageFunc - ReloadFunc ReloadFunc + AppVersion string + App *backend.App + MainWindow fyne.Window + NavHandler NavigationHandler + CurPageFunc CurPageFunc + ReloadFunc func() + RefreshPageFunc func() escapablePopUp *widget.PopUp haveModal bool @@ -559,6 +558,7 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map copy(eq.BandGains[:], c.App.Config.LocalPlayback.GraphicEqualizerBands) c.App.LocalPlayer.SetEqualizer(eq) } + dlg.OnPageNeedsRefresh = c.RefreshPageFunc pop := widget.NewModalPopUp(dlg, c.MainWindow.Canvas()) dlg.OnDismiss = func() { pop.Hide() diff --git a/ui/dialogs/settingsdialog.go b/ui/dialogs/settingsdialog.go index 2690ea0d..fb33ed66 100644 --- a/ui/dialogs/settingsdialog.go +++ b/ui/dialogs/settingsdialog.go @@ -34,6 +34,7 @@ type SettingsDialog struct { OnThemeSettingChanged func() OnDismiss func() OnEqualizerSettingsChanged func() + OnPageNeedsRefresh func() config *backend.Config audioDevices []mpv.AudioDevice @@ -185,6 +186,14 @@ func (s *SettingsDialog) createGeneralTab(canSaveQueueToServer bool) *container. trackNotif := widget.NewCheckWithData("Show notification on track change", binding.BindBool(&s.config.Application.ShowTrackChangeNotification)) + albumGridYears := widget.NewCheck("Show year in album grid cards", func(b bool) { + s.config.AlbumsPage.ShowYears = b + s.config.FavoritesPage.ShowAlbumYears = b + if s.OnPageNeedsRefresh != nil { + s.OnPageNeedsRefresh() + } + }) + albumGridYears.Checked = s.config.AlbumsPage.ShowYears // Scrobble settings @@ -276,6 +285,7 @@ func (s *SettingsDialog) createGeneralTab(canSaveQueueToServer bool) *container. container.NewHBox(systemTrayEnable, closeToTray), saveQueueHBox, trackNotif, + albumGridYears, s.newSectionSeparator(), widget.NewRichText(&widget.TextSegment{Text: "Scrobbling", Style: util.BoldRichTextStyle}), diff --git a/ui/mainwindow.go b/ui/mainwindow.go index 5e5229b8..0335eb28 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -85,6 +85,7 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, m.Controller.NavHandler = m.Router.NavigateTo m.Controller.ReloadFunc = m.BrowsingPane.Reload m.Controller.CurPageFunc = m.BrowsingPane.CurrentPage + m.Controller.RefreshPageFunc = m.BrowsingPane.RefreshPage m.BottomPanel = NewBottomPanel(app.PlaybackManager, app.ImageManager, m.Controller) m.container = container.NewBorder(nil, m.BottomPanel, nil, nil, m.BrowsingPane) diff --git a/ui/theme/theme.go b/ui/theme/theme.go index 0dab7eef..6738f7cf 100644 --- a/ui/theme/theme.go +++ b/ui/theme/theme.go @@ -26,6 +26,9 @@ const ( ColorNameIconButton fyne.ThemeColorName = "IconButton" ColorNameHoveredIconButton fyne.ThemeColorName = "HoveredIconButton" ColorNameNowPlayingPanel fyne.ThemeColorName = "NowPlayingPanel" + + SizeNameSubText fyne.ThemeSizeName = "subText" // in between Text and Caption + SizeNameSuffixText fyne.ThemeSizeName = "suffixText" // a tiny bit smaller than subText ) var ( @@ -243,6 +246,11 @@ func (m *MyTheme) Font(style fyne.TextStyle) fyne.Resource { } func (m *MyTheme) Size(name fyne.ThemeSizeName) float32 { + if name == SizeNameSubText { + return 13 + } else if name == SizeNameSuffixText { + return 12 + } return theme.DefaultTheme().Size(name) } diff --git a/ui/widgets/gridview.go b/ui/widgets/gridview.go index 5776413a..5babca02 100644 --- a/ui/widgets/gridview.go +++ b/ui/widgets/gridview.go @@ -3,6 +3,7 @@ package widgets import ( "context" "fmt" + "strconv" "sync" "github.com/dweymouth/supersonic/backend/mediaprovider" @@ -51,13 +52,17 @@ type gridViewAlbumIterator struct { func (g gridViewAlbumIterator) NextN(n int) []GridViewItemModel { albums := g.iter.NextN(n) return sharedutil.MapSlice(albums, func(al *mediaprovider.Album) GridViewItemModel { - return GridViewItemModel{ + model := GridViewItemModel{ Name: al.Name, ID: al.ID, CoverArtID: al.CoverArtID, Secondary: al.ArtistNames, SecondaryIDs: al.ArtistIDs, } + if al.Year > 0 { + model.Suffix = strconv.Itoa(al.Year) + } + return model }) } @@ -92,6 +97,8 @@ func NewGridViewArtistIterator(iter mediaprovider.ArtistIterator) GridViewIterat type GridView struct { widget.BaseWidget + ShowSuffix bool + stateMutex sync.RWMutex fetchCancel context.CancelFunc GridViewState @@ -322,9 +329,11 @@ func (g *GridView) doUpdateItemCard(itemIdx int, card *GridViewItem) { card.ItemIndex = itemIdx g.itemForIndex[itemIdx] = card card.Cover.Im.PlaceholderIcon = g.Placeholder + card.ShowSuffix = g.ShowSuffix if !card.NeedsUpdate(item) && card.ItemIndex == itemIdx { // nothing to do g.stateMutex.Unlock() + card.Refresh() return } g.stateMutex.Unlock() diff --git a/ui/widgets/gridviewitem.go b/ui/widgets/gridviewitem.go index d497441d..ef7c85ce 100644 --- a/ui/widgets/gridviewitem.go +++ b/ui/widgets/gridviewitem.go @@ -5,9 +5,6 @@ import ( "image/color" "slices" - "github.com/dweymouth/supersonic/res" - "github.com/dweymouth/supersonic/ui/util" - "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" @@ -15,6 +12,10 @@ import ( "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" + + "github.com/dweymouth/supersonic/res" + myTheme "github.com/dweymouth/supersonic/ui/theme" + "github.com/dweymouth/supersonic/ui/util" ) var _ fyne.Widget = (*GridViewItem)(nil) @@ -131,15 +132,19 @@ type GridViewItemModel struct { CoverArtID string Secondary []string SecondaryIDs []string + Suffix string } type GridViewItem struct { widget.BaseWidget + ShowSuffix bool + itemID string secondaryIDs []string primaryText *widget.Hyperlink secondaryText *MultiHyperlink + suffix string container *fyne.Container focused bool focusRect *canvas.Rectangle @@ -164,8 +169,10 @@ func NewGridViewItem(placeholderResource fyne.Resource) *GridViewItem { secondaryText: NewMultiHyperlink(), Cover: newCoverImage(placeholderResource), } - g.primaryText.TextStyle.Bold = true g.primaryText.Truncation = fyne.TextTruncateEllipsis + g.primaryText.TextStyle.Bold = true + g.secondaryText.SizeName = myTheme.SizeNameSubText + g.secondaryText.SuffixSizeName = myTheme.SizeNameSuffixText g.ExtendBaseWidget(g) g.Cover.OnPlay = func() { if g.OnPlay != nil { @@ -195,7 +202,7 @@ func NewGridViewItem(placeholderResource fyne.Resource) *GridViewItem { } func (g *GridViewItem) createContainer() { - info := container.New(layout.NewCustomPaddedVBoxLayout(theme.Padding()-16), g.primaryText, g.secondaryText) + info := container.New(layout.NewCustomPaddedVBoxLayout(theme.Padding()-17), g.primaryText, g.secondaryText) g.focusRect = canvas.NewRectangle(color.Transparent) g.focusRect.StrokeWidth = 3 coverStack := container.NewStack(g.Cover, g.focusRect) @@ -205,7 +212,8 @@ func (g *GridViewItem) createContainer() { } func (g *GridViewItem) NeedsUpdate(model GridViewItemModel) bool { - return g.itemID != model.ID || !slices.Equal(g.secondaryIDs, model.SecondaryIDs) + return g.itemID != model.ID || !slices.Equal(g.secondaryIDs, model.SecondaryIDs) || + g.secondaryText.Suffix != model.Suffix } func (g *GridViewItem) Update(model GridViewItemModel) { @@ -213,6 +221,11 @@ func (g *GridViewItem) Update(model GridViewItemModel) { g.secondaryIDs = model.SecondaryIDs g.primaryText.SetText(model.Name) g.secondaryText.BuildSegments(model.Secondary, model.SecondaryIDs) + if g.ShowSuffix { + g.secondaryText.Suffix = model.Suffix + } else { + g.secondaryText.Suffix = "" + } g.secondaryText.Refresh() g.Cover.ResetPlayButton() if g.focused { @@ -222,9 +235,15 @@ func (g *GridViewItem) Update(model GridViewItemModel) { } func (g *GridViewItem) Refresh() { + if g.ShowSuffix && g.secondaryText.Suffix == "" && g.suffix != "" { + g.secondaryText.Suffix = g.suffix + g.secondaryText.Refresh() + } else if !g.ShowSuffix && g.secondaryText.Suffix != "" { + g.secondaryText.Suffix = "" + g.secondaryText.Refresh() + } g.focusRect.StrokeColor = util.MakeOpaque(theme.FocusColor()) g.focusRect.Hidden = !g.focused - g.BaseWidget.Refresh() } func (g *GridViewItem) ItemID() string { diff --git a/ui/widgets/multihyperlink.go b/ui/widgets/multihyperlink.go index da88492e..a76968dc 100644 --- a/ui/widgets/multihyperlink.go +++ b/ui/widgets/multihyperlink.go @@ -11,8 +11,16 @@ type MultiHyperlink struct { widget.BaseWidget Segments []MultiHyperlinkSegment + + // Suffix string that is appended (with · separator) + // only if there is enough room + Suffix string + OnTapped func(string) + SizeName fyne.ThemeSizeName + SuffixSizeName fyne.ThemeSizeName + minSegWidthCached float32 minHeightCached float32 separatorWCached float32 @@ -20,7 +28,10 @@ type MultiHyperlink struct { // TODO: Once https://github.com/fyne-io/fyne/issues/4336 is resolved, // we can switch to the much cleaner RichText implementation //provider *widget.RichText - content *fyne.Container + + objects []fyne.CanvasObject + suffixLabel *widget.RichText + content *fyne.Container } type MultiHyperlinkSegment struct { @@ -52,14 +63,14 @@ func (m *MultiHyperlink) BuildSegments(texts, links []string) { func (c *MultiHyperlink) getMinSegWidth() float32 { if c.minSegWidthCached == 0 { - c.minSegWidthCached = fyne.MeasureText(", W", theme.TextSize(), fyne.TextStyle{}).Width + c.minSegWidthCached = fyne.MeasureText(", W", theme.Size(c.sizeName()), fyne.TextStyle{}).Width } return c.minSegWidthCached } func (c *MultiHyperlink) getSeparatorWidth() float32 { if c.separatorWCached == 0 { - c.separatorWCached = fyne.MeasureText(",", theme.TextSize(), fyne.TextStyle{}).Width + c.separatorWCached = fyne.MeasureText(",", theme.Size(c.sizeName()), fyne.TextStyle{}).Width } return c.separatorWCached } @@ -71,7 +82,7 @@ func (c *MultiHyperlink) layoutObjects() { } x := float32(0) width := c.Size().Width - l := len(c.content.Objects) + l := len(c.objects) var i int // at end of loop should be index of last seg that was laid out for display var seg MultiHyperlinkSegment @@ -87,9 +98,9 @@ func (c *MultiHyperlink) layoutObjects() { appendingSegments := 2*i >= l var obj fyne.CanvasObject if !appendingSegments { - obj = c.content.Objects[2*i] + obj = c.objects[2*i] } else if i > 0 { - c.content.Objects = append(c.content.Objects, c.newSeparatorLabel()) + c.objects = append(c.objects, c.newSeparatorLabel()) } if seg.LinkID == "" { obj = c.updateOrReplaceLabel(obj, seg.Text) @@ -97,14 +108,14 @@ func (c *MultiHyperlink) layoutObjects() { obj = c.updateOrReplaceHyperlink(obj, seg.Text, seg.LinkID) } if appendingSegments { - c.content.Objects = append(c.content.Objects, obj) + c.objects = append(c.objects, obj) } else { - c.content.Objects[2*i] = obj + c.objects[2*i] = obj } if i > 0 { // move and resize separator - obj = c.content.Objects[2*i-1] + obj = c.objects[2*i-1] ms := obj.MinSize() obj.Resize(ms) obj.Move(fyne.NewPos(x-ms.Width+c.getSeparatorWidth()+1, 0)) // this is really ugly @@ -112,8 +123,8 @@ func (c *MultiHyperlink) layoutObjects() { } // move and resize text object // extra +3 to textW gives it just enough space to not trigger ellipsis truncation - textW := fyne.MeasureText(seg.Text, theme.TextSize(), fyne.TextStyle{}).Width + theme.Padding()*2 + theme.InnerPadding() + 3 - obj = c.content.Objects[2*i] + textW := fyne.MeasureText(seg.Text, theme.Size(c.sizeName()), fyne.TextStyle{}).Width + theme.Padding()*2 + theme.InnerPadding() + 3 + obj = c.objects[2*i] ms := obj.MinSize() w := fyne.Min(width-x, textW) obj.Resize(fyne.NewSize(w, ms.Height)) @@ -122,17 +133,39 @@ func (c *MultiHyperlink) layoutObjects() { } i += 1 - c.content.Objects = c.content.Objects[:2*i-1] + c.content.Objects = c.objects[:2*i-1] + if i == len(c.Segments) && c.Suffix != "" { + if c.suffixLabel == nil { + c.suffixLabel = widget.NewRichTextWithText("· " + c.Suffix) + } else { + c.suffixLabel.Segments[0].(*widget.TextSegment).Text = "· " + c.Suffix + } + sizeName := c.sizeName() + if c.SuffixSizeName != "" { + sizeName = c.SuffixSizeName + } + // TODO: the magic numbers to get exact positioning here are gross + c.suffixLabel.Segments[0].(*widget.TextSegment).Style.SizeName = sizeName + innerPad2 := theme.InnerPadding() * 2 + if x+c.suffixLabel.MinSize().Width-innerPad2*1.3 < width { + y := theme.Size(c.sizeName()) - theme.Size(sizeName) + c.suffixLabel.Move(fyne.NewPos(x-innerPad2+1, y)) + c.content.Objects = append(c.content.Objects, c.suffixLabel) + } + } } func (c *MultiHyperlink) updateOrReplaceLabel(obj fyne.CanvasObject, text string) fyne.CanvasObject { if obj != nil { - if label, ok := obj.(*widget.Label); ok { - label.Text = text + if label, ok := obj.(*widget.RichText); ok { + ts := label.Segments[0].(*widget.TextSegment) + ts.Text = text + ts.Style.SizeName = c.sizeName() return label } } - l := widget.NewLabel(text) + l := widget.NewRichTextWithText(text) + l.Segments[0].(*widget.TextSegment).Style.SizeName = c.sizeName() l.Truncation = fyne.TextTruncateEllipsis return l } @@ -141,22 +174,34 @@ func (c *MultiHyperlink) updateOrReplaceHyperlink(obj fyne.CanvasObject, text, l if obj != nil { if l, ok := obj.(*widget.Hyperlink); ok { l.Text = text + l.SizeName = c.sizeName() l.OnTapped = func() { c.onSegmentTapped(link) } return l } } l := widget.NewHyperlink(text, nil) + l.SizeName = c.sizeName() l.Truncation = fyne.TextTruncateEllipsis l.OnTapped = func() { c.onSegmentTapped(link) } return l } -func (c *MultiHyperlink) newSeparatorLabel() *widget.Label { - return widget.NewLabel(", ") +func (c *MultiHyperlink) newSeparatorLabel() *widget.RichText { + rt := widget.NewRichTextWithText(", ") + rt.Segments[0].(*widget.TextSegment).Style.SizeName = c.sizeName() + return rt +} + +func (c *MultiHyperlink) sizeName() fyne.ThemeSizeName { + if c.SizeName == "" { + return theme.SizeNameText + } + return c.SizeName } /*** * RichText implementation + * TODO: add support for SizeName, suffix func (c *MultiHyperlink) syncSegments() { l := len(c.provider.Segments)