1/*
2 * Copyright 2007-2009 Stephan A��mus <superstippi@gmx.de>.
3 * All rights reserved. Distributed under the terms of the MIT License.
4 */
5
6
7#include "PlaylistListView.h"
8
9#include <new>
10#include <stdio.h>
11
12#include <Autolock.h>
13#include <Catalog.h>
14#include <GradientLinear.h>
15#include <MenuItem.h>
16#include <Message.h>
17#include <PopUpMenu.h>
18#include <ScrollBar.h>
19#include <ScrollView.h>
20#include <Shape.h>
21#include <Window.h>
22
23#include "CommandStack.h"
24#include "Controller.h"
25#include "ControllerObserver.h"
26#include "CopyPLItemsCommand.h"
27#include "DurationToString.h"
28#include "ImportPLItemsCommand.h"
29#include "ListViews.h"
30#include "MovePLItemsCommand.h"
31#include "PlaybackState.h"
32#include "Playlist.h"
33#include "PlaylistItem.h"
34#include "PlaylistObserver.h"
35#include "RandomizePLItemsCommand.h"
36#include "RemovePLItemsCommand.h"
37
38#undef B_TRANSLATION_CONTEXT
39#define B_TRANSLATION_CONTEXT "MediaPlayer-PlaylistListView"
40
41using std::nothrow;
42
43
44enum {
45	DISPLAY_NAME	= 0,
46	DISPLAY_PATH	= 1,
47	M_ADD_SORTED,
48	M_ADD_UNSORTED
49};
50
51
52static float
53playback_mark_size(const font_height& fh)
54{
55	return ceilf(fh.ascent * 0.7);
56}
57
58
59static float
60text_offset(const font_height& fh)
61{
62	return ceilf(fh.ascent * 0.8);
63}
64
65
66class PlaylistListView::Item : public SimpleItem,
67	public PlaylistItem::Listener {
68public:
69								Item(PlaylistItem* item);
70	virtual						~Item();
71
72			void				Draw(BView* owner, BRect frame,
73									const font_height& fh,
74									bool tintedLine, uint32 mode,
75									bool active,
76									uint32 playbackState);
77
78	virtual	void				ItemChanged(const PlaylistItem* item);
79
80#if __GNUC__ == 2
81	virtual	void				Draw(BView* owner, BRect frame, uint32 flags);
82#else
83			using SimpleItem::Draw;
84#endif
85
86private:
87			PlaylistItemRef		fItem;
88
89};
90
91
92// #pragma mark -
93
94
95PlaylistListView::Item::Item(PlaylistItem* item)
96	:
97	SimpleItem(item->Name().String()),
98	fItem(item)
99{
100	fItem->AddListener(this);
101}
102
103
104PlaylistListView::Item::~Item()
105{
106	fItem->RemoveListener(this);
107}
108
109
110void
111PlaylistListView::Item::Draw(BView* owner, BRect frame, const font_height& fh,
112	bool tintedLine, uint32 mode, bool active, uint32 playbackState)
113{
114	rgb_color color = ui_color(B_LIST_BACKGROUND_COLOR);
115
116	if (IsSelected())
117		color = ui_color(B_LIST_SELECTED_BACKGROUND_COLOR);
118	if (tintedLine)
119		color = tint_color(color, 1.04);
120	// background
121	owner->SetLowColor(color);
122	owner->FillRect(frame, B_SOLID_LOW);
123	// label
124	if (IsSelected())
125		owner->SetHighColor(ui_color(B_LIST_SELECTED_ITEM_TEXT_COLOR));
126	else
127		owner->SetHighColor(ui_color(B_LIST_ITEM_TEXT_COLOR));
128	const char* text = Text();
129	switch (mode) {
130		case DISPLAY_NAME:
131			// TODO
132			break;
133		case DISPLAY_PATH:
134			// TODO
135			break;
136		default:
137			break;
138	}
139
140	float playbackMarkSize = playback_mark_size(fh);
141	float textOffset = text_offset(fh);
142
143	char buffer[64];
144	bigtime_t duration = fItem->Duration();
145	duration /= 1000000;
146	duration_to_string(duration, buffer, sizeof(buffer));
147
148	BString truncatedDuration(buffer);
149	owner->TruncateString(&truncatedDuration, B_TRUNCATE_END,
150		frame.Width() - playbackMarkSize - textOffset);
151	float truncatedWidth = owner->StringWidth(truncatedDuration.String());
152	owner->DrawString(truncatedDuration.String(),
153		BPoint(frame.right - truncatedWidth,
154			floorf(frame.top + frame.bottom + fh.ascent) / 2 - 1));
155
156	BString truncatedString(text);
157	owner->TruncateString(&truncatedString, B_TRUNCATE_MIDDLE,
158		frame.Width() - playbackMarkSize - textOffset - truncatedWidth);
159	owner->DrawString(truncatedString.String(),
160		BPoint(frame.left + playbackMarkSize + textOffset,
161			floorf(frame.top + frame.bottom + fh.ascent) / 2 - 1));
162
163	// playmark
164	if (active) {
165		rgb_color green = (rgb_color){ 0, 255, 0, 255 };
166		if (playbackState != PLAYBACK_STATE_PLAYING)
167			green = tint_color(color, B_DARKEN_1_TINT);
168
169		BRect r(0, 0, playbackMarkSize, playbackMarkSize);
170		r.OffsetTo(frame.left + 4,
171			ceilf((frame.top + frame.bottom - playbackMarkSize) / 2));
172
173		uint32 flags = owner->Flags();
174		owner->SetFlags(flags | B_SUBPIXEL_PRECISE);
175
176		BShape shape;
177		shape.MoveTo(r.LeftTop());
178		shape.LineTo(r.LeftBottom());
179		shape.LineTo(BPoint(r.right, (r.top + r.bottom) / 2));
180		shape.Close();
181
182		owner->MovePenTo(B_ORIGIN);
183		owner->FillShape(&shape);
184
185		shape.Clear();
186		r.InsetBy(1, 1);
187		shape.MoveTo(r.LeftTop());
188		shape.LineTo(r.LeftBottom());
189		shape.LineTo(BPoint(r.right, (r.top + r.bottom) / 2));
190		shape.Close();
191
192		BGradientLinear gradient;
193		gradient.SetStart(r.LeftTop());
194		gradient.SetEnd(r.LeftBottom());
195		gradient.AddColor(tint_color(green, B_LIGHTEN_1_TINT), 0);
196		gradient.AddColor(tint_color(green, B_DARKEN_1_TINT), 255.0);
197
198		owner->FillShape(&shape, gradient);
199
200		owner->SetFlags(flags);
201	}
202}
203
204
205void
206PlaylistListView::Item::ItemChanged(const PlaylistItem* item)
207{
208	// TODO: Invalidate
209}
210
211
212#if __GNUC__ == 2
213
214void
215PlaylistListView::Item::Draw(BView* owner, BRect frame, uint32 flags)
216{
217	SimpleItem::Draw(owner, frame, flags);
218}
219
220#endif
221
222
223// #pragma mark -
224
225
226PlaylistListView::PlaylistListView(BRect frame, Playlist* playlist,
227		Controller* controller, CommandStack* stack)
228	:
229	SimpleListView(frame, "playlist listview", NULL),
230
231	fPlaylist(playlist),
232	fPlaylistObserver(new PlaylistObserver(this)),
233
234	fController(controller),
235	fControllerObserver(new ControllerObserver(this,
236			OBSERVE_PLAYBACK_STATE_CHANGES)),
237
238	fCommandStack(stack),
239
240	fCurrentPlaylistIndex(-1),
241	fPlaybackState(PLAYBACK_STATE_STOPPED),
242
243	fLastClickedItem(NULL)
244{
245	fPlaylist->AddListener(fPlaylistObserver);
246	fController->AddListener(fControllerObserver);
247	_AddDropContextMenu();
248
249	SetFlags(Flags() | B_SUBPIXEL_PRECISE);
250}
251
252
253PlaylistListView::~PlaylistListView()
254{
255	for (int32 i = CountItems() - 1; i >= 0; i--)
256		_RemoveItem(i);
257	fPlaylist->RemoveListener(fPlaylistObserver);
258	delete fPlaylistObserver;
259	fController->RemoveListener(fControllerObserver);
260	delete fControllerObserver;
261}
262
263
264void
265PlaylistListView::AttachedToWindow()
266{
267	_FullSync();
268	SimpleListView::AttachedToWindow();
269
270	GetFontHeight(&fFontHeight);
271	MakeFocus(true);
272}
273
274
275void
276PlaylistListView::MessageReceived(BMessage* message)
277{
278	switch (message->what) {
279		// PlaylistObserver messages
280		case MSG_PLAYLIST_ITEM_ADDED:
281		{
282			PlaylistItem* item;
283			int32 index;
284			if (message->FindPointer("item", (void**)&item) == B_OK
285				&& message->FindInt32("index", &index) == B_OK)
286				_AddItem(item, index);
287			break;
288		}
289		case MSG_PLAYLIST_ITEM_REMOVED:
290		{
291			int32 index;
292			if (message->FindInt32("index", &index) == B_OK)
293				_RemoveItem(index);
294			break;
295		}
296		case MSG_PLAYLIST_ITEMS_SORTED:
297			_FullSync();
298			break;
299		case MSG_PLAYLIST_CURRENT_ITEM_CHANGED:
300		{
301			int32 index;
302			if (message->FindInt32("index", &index) == B_OK)
303				_SetCurrentPlaylistIndex(index);
304			break;
305		}
306		case MSG_PLAYLIST_IMPORT_FAILED:
307			break;
308
309		// ControllerObserver messages
310		case MSG_CONTROLLER_PLAYBACK_STATE_CHANGED:
311		{
312			uint32 state;
313			if (message->FindInt32("state", (int32*)&state) == B_OK)
314				_SetPlaybackState(state);
315			break;
316		}
317
318		case B_SIMPLE_DATA:
319			if (message->HasRef("refs"))
320				ItemsReceived(message, fDropIndex);
321			else if (message->HasPointer("list"))
322				SimpleListView::MessageReceived(message);
323			break;
324		case B_REFS_RECEIVED:
325			ItemsReceived(message, fDropIndex);
326			break;
327
328		default:
329			SimpleListView::MessageReceived(message);
330			break;
331	}
332}
333
334
335void
336PlaylistListView::MouseDown(BPoint where)
337{
338	if (!IsFocus())
339		MakeFocus(true);
340
341	int32 clicks;
342	if (Window()->CurrentMessage()->FindInt32("clicks", &clicks) < B_OK)
343		clicks = 1;
344
345	bool handled = false;
346
347	float playbackMarkSize = playback_mark_size(fFontHeight);
348	float textOffset = text_offset(fFontHeight);
349
350	for (int32 i = 0;
351		Item* item = dynamic_cast<Item*>(ItemAt(i)); i++) {
352		BRect r = ItemFrame(i);
353		if (r.Contains(where)) {
354			if (clicks == 2) {
355				// only do something if user clicked the same item twice
356				if (fLastClickedItem == item) {
357					BAutolock _(fPlaylist);
358					fPlaylist->SetCurrentItemIndex(i, true);
359					handled = true;
360				}
361			} else {
362				// remember last clicked item
363				fLastClickedItem = item;
364				if (i == fCurrentPlaylistIndex) {
365					r.right = r.left + playbackMarkSize + textOffset;
366					if (r.Contains (where)) {
367						fController->TogglePlaying();
368						handled = true;
369					}
370				}
371			}
372			break;
373		}
374	}
375
376	if (!handled)
377		SimpleListView::MouseDown(where);
378}
379
380
381void
382PlaylistListView::KeyDown(const char* bytes, int32 numBytes)
383{
384	if (numBytes < 1)
385		return;
386
387	BMessage* msg = Window()->CurrentMessage();
388	uint32 modifier = msg->FindInt32("modifiers");
389
390	int32 count;
391	int32 index;
392
393	switch (bytes[0]) {
394		case B_SPACE:
395			fController->TogglePlaying();
396			break;
397
398		case B_BACKSPACE:
399		case B_DELETE:
400			RemoveSelected();
401			break;
402
403		case B_ENTER:
404			count = CountItems();
405			if (count == 0)
406				break;
407			index = CurrentSelection(0);
408			if (index < 0)
409				break;
410			fPlaylist->SetCurrentItemIndex(index, true);
411			fController->Play();
412			break;
413
414		case B_ESCAPE:
415			fController->Stop();
416			break;
417
418		case B_RIGHT_ARROW:
419			if ((modifier & B_SHIFT_KEY) != 0)
420				_Wind(30000000LL, 5);
421			else
422				_Wind(5000000LL, 1);
423			break;
424
425		case B_LEFT_ARROW:
426			if ((modifier & B_SHIFT_KEY) != 0)
427				_Wind(-30000000LL, -5);
428			else
429				_Wind(-5000000LL, -1);
430			break;
431		default:
432			DragSortableListView::KeyDown(bytes, numBytes);
433	}
434}
435
436
437void
438PlaylistListView::SkipBackward()
439{
440	BAutolock _(fPlaylist);
441	int32 index = fPlaylist->CurrentItemIndex() - 1;
442	if (index < 0)
443		index = 0;
444	fPlaylist->SetCurrentItemIndex(index, true);
445}
446
447
448void
449PlaylistListView::SkipForward()
450{
451	BAutolock _(fPlaylist);
452	int32 index = fPlaylist->CurrentItemIndex() + 1;
453	if (index >= fPlaylist->CountItems())
454		index = fPlaylist->CountItems() - 1;
455	fPlaylist->SetCurrentItemIndex(index, true);
456}
457
458
459void
460PlaylistListView::_Wind(bigtime_t howMuch, int64 frames)
461{
462	if (!fController->Lock())
463		return;
464
465	if (frames != 0 && !fController->IsPlaying()) {
466		int64 newFrame = fController->CurrentFrame() + frames;
467		fController->SetFramePosition(newFrame);
468	} else {
469		bigtime_t seekTime = fController->TimePosition() + howMuch;
470		if (seekTime < 0) {
471			SkipBackward();
472		} else if (seekTime > fController->TimeDuration()) {
473			SkipForward();
474		} else
475			fController->SetTimePosition(seekTime);
476	}
477
478	fController->Unlock();
479}
480
481
482void
483PlaylistListView::MoveItems(const BList& indices, int32 toIndex)
484{
485	fCommandStack->Perform(new (nothrow) MovePLItemsCommand(fPlaylist,
486		indices, toIndex));
487}
488
489
490void
491PlaylistListView::CopyItems(const BList& indices, int32 toIndex)
492{
493	fCommandStack->Perform(new (nothrow) CopyPLItemsCommand(fPlaylist,
494		indices, toIndex));
495}
496
497
498void
499PlaylistListView::RemoveItemList(const BList& indices)
500{
501	RemoveItemList(indices, false);
502}
503
504
505void
506PlaylistListView::DrawListItem(BView* owner, int32 index, BRect frame) const
507{
508	if (Item* item = dynamic_cast<Item*>(ItemAt(index))) {
509		item->Draw(owner, frame, fFontHeight, index % 2,
510			DISPLAY_NAME, index == fCurrentPlaylistIndex, fPlaybackState);
511	}
512}
513
514
515void
516PlaylistListView::ItemsReceived(const BMessage* message, int32 appendIndex)
517{
518	BPoint dropPoint;
519	bool sorting = false;
520	entry_ref ref;
521
522	if (message->FindRef("refs", 1, &ref) == B_OK
523		&& message->FindPoint("_drop_point_", &dropPoint) == B_OK
524		&& message->GetInt32("buttons", 0) == 2)
525		if (_ShowDropContextMenu(dropPoint) == M_ADD_SORTED)
526			sorting = true;
527
528	if (fCommandStack->Perform(new (nothrow) ImportPLItemsCommand(fPlaylist,
529			message, appendIndex, sorting)) != B_OK) {
530		fPlaylist->NotifyImportFailed();
531	}
532}
533
534
535void
536PlaylistListView::Randomize()
537{
538	int32 count = CountItems();
539	if (count == 0)
540		return;
541
542	BList indices;
543
544	// add current selection
545	count = 0;
546	while (true) {
547		int32 index = CurrentSelection(count);
548		if (index < 0)
549			break;
550		if (!indices.AddItem((void*)(addr_t)index))
551			return;
552		count++;
553	}
554
555	// was anything selected?
556	if (count == 0) {
557		// no selection, simply add all items
558		count = CountItems();
559		for (int32 i = 0; i < count; i++) {
560			if (!indices.AddItem((void*)(addr_t)i))
561				return;
562		}
563	}
564
565	fCommandStack->Perform(new (nothrow) RandomizePLItemsCommand(fPlaylist,
566		indices));
567}
568
569
570void
571PlaylistListView::RemoveSelectionToTrash()
572{
573	BList indices;
574	GetSelectedItems(indices);
575	RemoveItemList(indices, true);
576}
577
578
579void
580PlaylistListView::RemoveToTrash(int32 index)
581{
582	BList indices;
583	indices.AddItem((void*)(addr_t)index);
584	RemoveItemList(indices, true);
585}
586
587
588void
589PlaylistListView::RemoveItemList(const BList& indices, bool intoTrash)
590{
591	fCommandStack->Perform(new (nothrow) RemovePLItemsCommand(fPlaylist,
592		indices, intoTrash));
593}
594
595
596// #pragma mark -
597
598
599void
600PlaylistListView::_FullSync()
601{
602	if (!fPlaylist->Lock())
603		return;
604
605	// detaching the scrollbar temporarily will
606	// make this much quicker
607	BScrollBar* scrollBar = ScrollBar(B_VERTICAL);
608	if (scrollBar) {
609		if (Window())
610			Window()->UpdateIfNeeded();
611		scrollBar->SetTarget((BView*)NULL);
612	}
613
614	for (int32 i = CountItems() - 1; i >= 0; i--)
615		_RemoveItem(i);
616
617	int32 count = fPlaylist->CountItems();
618	for (int32 i = 0; i < count; i++)
619		_AddItem(fPlaylist->ItemAt(i), i);
620
621	_SetCurrentPlaylistIndex(fPlaylist->CurrentItemIndex());
622	_SetPlaybackState(fController->PlaybackState());
623
624	// reattach scrollbar and sync it by calling FrameResized()
625	if (scrollBar) {
626		scrollBar->SetTarget(this);
627		FrameResized(Bounds().Width(), Bounds().Height());
628	}
629
630	fPlaylist->Unlock();
631}
632
633
634void
635PlaylistListView::_AddItem(PlaylistItem* _item, int32 index)
636{
637	if (_item == NULL)
638		return;
639
640	Item* item = new (nothrow) Item(_item);
641	if (item != NULL)
642		AddItem(item, index);
643}
644
645
646void
647PlaylistListView::_RemoveItem(int32 index)
648{
649	delete RemoveItem(index);
650}
651
652
653void
654PlaylistListView::_SetCurrentPlaylistIndex(int32 index)
655{
656	if (fCurrentPlaylistIndex == index)
657		return;
658
659	InvalidateItem(fCurrentPlaylistIndex);
660	fCurrentPlaylistIndex = index;
661	InvalidateItem(fCurrentPlaylistIndex);
662}
663
664
665void
666PlaylistListView::_SetPlaybackState(uint32 state)
667{
668	if (fPlaybackState == state)
669		return;
670
671	fPlaybackState = state;
672	InvalidateItem(fCurrentPlaylistIndex);
673}
674
675
676void
677PlaylistListView::_AddDropContextMenu()
678{
679	fDropContextMenu = new BPopUpMenu("DropContext");
680
681	fDropContextMenu->AddItem(new BMenuItem(B_TRANSLATE("Add sorted"),
682		new BMessage(M_ADD_SORTED)));
683	fDropContextMenu->AddItem(new BMenuItem(B_TRANSLATE("Add unsorted"),
684		new BMessage(M_ADD_UNSORTED)));
685}
686
687
688uint32
689PlaylistListView::_ShowDropContextMenu(BPoint dropPoint)
690{
691	BMenuItem* item;
692
693	item = fDropContextMenu->Go(dropPoint, true, true);
694	if (item != NULL)
695		return item->Command();
696	return 0;
697}
698
699
700