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