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