1package widgets 2 3import ( 4 "fmt" 5 "math" 6 "time" 7 8 "github.com/ambientsound/pms/api" 9 "github.com/ambientsound/pms/console" 10 "github.com/ambientsound/pms/song" 11 "github.com/ambientsound/pms/songlist" 12 "github.com/ambientsound/pms/style" 13 "github.com/ambientsound/pms/utils" 14 15 "github.com/gdamore/tcell" 16 "github.com/gdamore/tcell/views" 17) 18 19// SonglistWidget is a tcell widget which draws a Songlist on the screen. It 20// maintains a list of songlists which can be cycled through. 21type SonglistWidget struct { 22 api api.API 23 columns songlist.Columns 24 25 view views.View 26 viewport views.ViewPort 27 lastDraw time.Time 28 29 style.Styled 30 views.WidgetWatchers 31} 32 33func NewSonglistWidget(a api.API) (w *SonglistWidget) { 34 return &SonglistWidget{ 35 api: a, 36 } 37} 38 39func (w *SonglistWidget) drawNext(x, y, strmin, strmax int, runes []rune, style tcell.Style) int { 40 strmin = utils.Min(len(runes), strmin) 41 n := 0 42 for n < strmin { 43 w.viewport.SetContent(x, y, runes[n], nil, style) 44 n++ 45 x++ 46 } 47 for n < strmax { 48 w.viewport.SetContent(x, y, ' ', nil, style) 49 n++ 50 x++ 51 } 52 return x 53} 54 55func (w *SonglistWidget) drawOneTagLine(x, y, xmax int, s *song.Song, tag string, defaultStyle string, style tcell.Style, lineStyled bool) int { 56 if !lineStyled { 57 style = w.Style(defaultStyle) 58 } 59 60 runes := s.Tags[tag] 61 strmin := len(runes) 62 63 return w.drawNext(x, y, strmin, xmax+1, runes, style) 64} 65 66func (w *SonglistWidget) Panel() *songlist.Collection { 67 return w.api.Db().Panel() 68} 69 70func (w *SonglistWidget) List() songlist.Songlist { 71 return w.Panel().Current() 72} 73 74func (w *SonglistWidget) Draw() { 75 //console.Log("Draw() in songlist widget") 76 list := w.List() 77 if w.view == nil || list == nil || list.Songs() == nil { 78 console.Log("BUG: nil list, aborting draw!") 79 return 80 } 81 82 // Check if the current panel's songlist has changed. 83 if w.Panel().Updated().After(w.lastDraw) { 84 w.viewport.Resize(0, 0, -1, -1) 85 PostEventListChanged(w) 86 } else if list.Updated().Before(w.lastDraw) { 87 //console.Log("SonglistWidget::Draw(): not drawing, already drawn") 88 //return 89 } 90 91 //console.Log("SonglistWidget::Draw()") 92 93 // Make sure that the viewport matches the list size. 94 w.setViewportSize() 95 96 // Update draw time 97 w.lastDraw = time.Now() 98 99 _, ymin, xmax, ymax := w.viewport.GetVisible() 100 currentSong := w.api.Song() 101 xmax += 1 102 style := w.Style("default") 103 cursor := false 104 105 for y := ymin; y <= ymax; y++ { 106 107 lineStyled := true 108 s := list.Song(y) 109 if s == nil { 110 // Sometimes happens under race conditions; just abort drawing 111 console.Log("Attempting to draw nil song, aborting draw due to possible race condition.") 112 return 113 } 114 115 // Style based on song's role 116 cursor = y == list.Cursor() 117 switch { 118 case cursor: 119 style = w.Style("cursor") 120 case list.IndexAtSong(y, currentSong): 121 style = w.Style("currentSong") 122 case list.Selected(y): 123 style = w.Style("selection") 124 default: 125 style = w.Style("default") 126 lineStyled = false 127 } 128 129 x := 0 130 rightPadding := 1 131 132 // If all essential tags are missing, draw only the filename 133 if !s.HasOneOfTags("artist", "album", "title") { 134 w.drawOneTagLine(x, y, xmax+1, s, `file`, `allTagsMissing`, style, lineStyled) 135 continue 136 } 137 138 // If most essential tags are missing, but the title is present, draw only the title. 139 if !s.HasOneOfTags("artist", "album") { 140 w.drawOneTagLine(x, y, xmax+1, s, `title`, `mostTagsMissing`, style, lineStyled) 141 continue 142 } 143 144 // Draw each column separately 145 for col := 0; col < len(w.columns); col++ { 146 147 // Convert tag to runes 148 key := w.columns[col].Tag() 149 runes := s.Tags[key] 150 if !lineStyled { 151 style = w.Style(key) 152 } 153 154 if col+1 == len(w.columns) { 155 rightPadding = 0 156 } 157 158 strmax := w.columns[col].Width() 159 strmin := strmax - rightPadding 160 161 x = w.drawNext(x, y, strmin, strmax, runes, style) 162 } 163 } 164 165 w.PostEventWidgetContent(w) 166 PostEventScroll(w) 167} 168 169func (w *SonglistWidget) GetVisibleBoundaries() (ymin, ymax int) { 170 _, ymin, _, ymax = w.viewport.GetVisible() 171 return 172} 173 174// Width returns the widget width. 175func (w *SonglistWidget) Width() int { 176 _, _, xmax, _ := w.viewport.GetVisible() 177 return xmax 178} 179 180// Height returns the widget height. 181func (w *SonglistWidget) Height() int { 182 _, ymin, _, ymax := w.viewport.GetVisible() 183 return ymax - ymin 184} 185 186func (w *SonglistWidget) setViewportSize() { 187 x, y := w.Size() 188 w.viewport.SetContentSize(x, w.List().Len(), true) 189 w.viewport.SetSize(x, utils.Min(y, w.List().Len())) 190 w.validateViewport() 191} 192 193// validateViewport moves the visible viewport so that the cursor is made visible. 194// If the 'center' option is enabled, the viewport is centered on the cursor. 195func (w *SonglistWidget) validateViewport() { 196 list := w.List() 197 cursor := list.Cursor() 198 199 // Make the cursor visible 200 if !w.api.Options().BoolValue("center") { 201 w.viewport.MakeVisible(0, cursor) 202 return 203 } 204 205 // If 'center' is on, make the cursor centered. 206 half := w.Height() / 2 207 min := utils.Max(0, cursor-half) 208 max := utils.Min(list.Len()-1, cursor+half) 209 w.viewport.MakeVisible(0, min) 210 w.viewport.MakeVisible(0, max) 211} 212 213func (w *SonglistWidget) Resize() { 214} 215 216func (m *SonglistWidget) HandleEvent(ev tcell.Event) bool { 217 return false 218} 219 220func (w *SonglistWidget) SetView(v views.View) { 221 w.view = v 222 w.viewport.SetView(w.view) 223} 224 225func (w *SonglistWidget) Size() (int, int) { 226 return w.view.Size() 227} 228 229func (w *SonglistWidget) Name() string { 230 return w.List().Name() 231} 232 233// PositionReadout returns a combination of PositionLongReadout() and PositionShortReadout(). 234// FIXME: move this into a positionreadout fragment 235func (w *SonglistWidget) PositionReadout() string { 236 return fmt.Sprintf("%s %s", w.PositionLongReadout(), w.PositionShortReadout()) 237} 238 239// PositionLongReadout returns a formatted string containing the visible song 240// range as well as the total number of songs. 241// FIXME: move this into a positionreadout fragment 242func (w *SonglistWidget) PositionLongReadout() string { 243 ymin, ymax := w.GetVisibleBoundaries() 244 return fmt.Sprintf("%d,%d-%d/%d", w.List().Cursor()+1, ymin+1, ymax+1, w.List().Len()) 245} 246 247// PositionShortReadout returns a percentage indicator on how far the songlist is scrolled. 248// FIXME: move this into a positionreadout fragment 249func (w *SonglistWidget) PositionShortReadout() string { 250 ymin, ymax := w.GetVisibleBoundaries() 251 if ymin == 0 && ymax+1 == w.List().Len() { 252 return `All` 253 } 254 if ymin == 0 { 255 return `Top` 256 } 257 if ymax+1 == w.List().Len() { 258 return `Bot` 259 } 260 fraction := float64(float64(ymin) / float64(w.List().Len())) 261 percent := int(math.Floor(fraction * 100)) 262 return fmt.Sprintf("%2d%%", percent) 263} 264 265// SetColumns sets which columns that should be visible 266func (w *SonglistWidget) SetColumns(tags []string) { 267 xmax, _ := w.Size() 268 w.columns = w.List().Columns(tags) 269 w.columns.Expand(xmax) 270 //console.Log("SetColumns(%v) yields %+v", tags, w.columns) 271} 272 273// ScrollViewport scrolls the viewport by delta rows, as far as possible. 274// If movecursor is false, the cursor is kept pointing at the same song where 275// possible. If true, the cursor is moved delta rows. 276func (w *SonglistWidget) ScrollViewport(delta int, movecursor bool) { 277 // Do nothing if delta is zero 278 if delta == 0 { 279 return 280 } 281 282 if delta < 0 { 283 w.viewport.ScrollUp(-delta) 284 } else { 285 w.viewport.ScrollDown(delta) 286 } 287 288 if movecursor { 289 w.List().MoveCursor(delta) 290 } 291 292 w.validateCursor() 293} 294 295// validateCursor ensures the cursor is within the allowable area without moving 296// the viewport. 297func (w *SonglistWidget) validateCursor() { 298 ymin, ymax := w.GetVisibleBoundaries() 299 list := w.List() 300 cursor := list.Cursor() 301 302 if w.api.Options().BoolValue("center") { 303 // When 'center' is on, move cursor to the centre of the viewport 304 target := cursor 305 lowerbound := (ymin + ymax) / 2 306 upperbound := lowerbound 307 if ymin <= 0 { 308 // We are scrolled to the top, so the cursor is allowed to go above 309 // the middle of the viewport 310 lowerbound = 0 311 } 312 if ymax >= list.Len()-1 { 313 // We are scrolled to the bottom, so the cursor is allowed to go 314 // below the middle of the viewport 315 upperbound = list.Len() - 1 316 } 317 if target < lowerbound { 318 target = lowerbound 319 } 320 if target > upperbound { 321 target = upperbound 322 } 323 list.SetCursor(target) 324 } else { 325 // When 'center' is off, move cursor into the viewport 326 if cursor < ymin { 327 list.SetCursor(ymin) 328 } else if cursor > ymax { 329 list.SetCursor(ymax) 330 } 331 } 332} 333