xref: /haiku/src/apps/mediaplayer/playlist/PlaylistListView.cpp (revision 1cfb79d74aab3d3b8ba1e81e8c13a5e154856a59)
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 
41 using std::nothrow;
42 
43 
44 enum {
45 	DISPLAY_NAME	= 0,
46 	DISPLAY_PATH	= 1,
47 	M_ADD_SORTED,
48 	M_ADD_UNSORTED
49 };
50 
51 
52 static float
playback_mark_size(const font_height & fh)53 playback_mark_size(const font_height& fh)
54 {
55 	return ceilf(fh.ascent * 0.7);
56 }
57 
58 
59 static float
text_offset(const font_height & fh)60 text_offset(const font_height& fh)
61 {
62 	return ceilf(fh.ascent * 0.8);
63 }
64 
65 
66 class PlaylistListView::Item : public SimpleItem,
67 	public PlaylistItem::Listener {
68 public:
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 
86 private:
87 			PlaylistItemRef		fItem;
88 
89 };
90 
91 
92 // #pragma mark -
93 
94 
Item(PlaylistItem * item)95 PlaylistListView::Item::Item(PlaylistItem* item)
96 	:
97 	SimpleItem(item->Name().String()),
98 	fItem(item)
99 {
100 	fItem->AddListener(this);
101 }
102 
103 
~Item()104 PlaylistListView::Item::~Item()
105 {
106 	fItem->RemoveListener(this);
107 }
108 
109 
110 void
Draw(BView * owner,BRect frame,const font_height & fh,bool tintedLine,uint32 mode,bool active,uint32 playbackState)111 PlaylistListView::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 
205 void
ItemChanged(const PlaylistItem * item)206 PlaylistListView::Item::ItemChanged(const PlaylistItem* item)
207 {
208 	// TODO: Invalidate
209 }
210 
211 
212 #if __GNUC__ == 2
213 
214 void
Draw(BView * owner,BRect frame,uint32 flags)215 PlaylistListView::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 
PlaylistListView(BRect frame,Playlist * playlist,Controller * controller,CommandStack * stack)226 PlaylistListView::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 
~PlaylistListView()253 PlaylistListView::~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 
264 void
AttachedToWindow()265 PlaylistListView::AttachedToWindow()
266 {
267 	_FullSync();
268 	SimpleListView::AttachedToWindow();
269 
270 	GetFontHeight(&fFontHeight);
271 	MakeFocus(true);
272 }
273 
274 
275 void
MessageReceived(BMessage * message)276 PlaylistListView::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 
335 void
MouseDown(BPoint where)336 PlaylistListView::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 
381 void
KeyDown(const char * bytes,int32 numBytes)382 PlaylistListView::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 
437 void
SkipBackward()438 PlaylistListView::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 
448 void
SkipForward()449 PlaylistListView::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 
459 void
_Wind(bigtime_t howMuch,int64 frames)460 PlaylistListView::_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 
482 void
MoveItems(const BList & indices,int32 toIndex)483 PlaylistListView::MoveItems(const BList& indices, int32 toIndex)
484 {
485 	fCommandStack->Perform(new (nothrow) MovePLItemsCommand(fPlaylist,
486 		indices, toIndex));
487 }
488 
489 
490 void
CopyItems(const BList & indices,int32 toIndex)491 PlaylistListView::CopyItems(const BList& indices, int32 toIndex)
492 {
493 	fCommandStack->Perform(new (nothrow) CopyPLItemsCommand(fPlaylist,
494 		indices, toIndex));
495 }
496 
497 
498 void
RemoveItemList(const BList & indices)499 PlaylistListView::RemoveItemList(const BList& indices)
500 {
501 	RemoveItemList(indices, false);
502 }
503 
504 
505 void
DrawListItem(BView * owner,int32 index,BRect frame) const506 PlaylistListView::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 
515 void
ItemsReceived(const BMessage * message,int32 appendIndex)516 PlaylistListView::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 
535 void
Randomize()536 PlaylistListView::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 
570 void
RemoveSelectionToTrash()571 PlaylistListView::RemoveSelectionToTrash()
572 {
573 	BList indices;
574 	GetSelectedItems(indices);
575 	RemoveItemList(indices, true);
576 }
577 
578 
579 void
RemoveToTrash(int32 index)580 PlaylistListView::RemoveToTrash(int32 index)
581 {
582 	BList indices;
583 	indices.AddItem((void*)(addr_t)index);
584 	RemoveItemList(indices, true);
585 }
586 
587 
588 void
RemoveItemList(const BList & indices,bool intoTrash)589 PlaylistListView::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 
599 void
_FullSync()600 PlaylistListView::_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 
634 void
_AddItem(PlaylistItem * _item,int32 index)635 PlaylistListView::_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 
646 void
_RemoveItem(int32 index)647 PlaylistListView::_RemoveItem(int32 index)
648 {
649 	delete RemoveItem(index);
650 }
651 
652 
653 void
_SetCurrentPlaylistIndex(int32 index)654 PlaylistListView::_SetCurrentPlaylistIndex(int32 index)
655 {
656 	if (fCurrentPlaylistIndex == index)
657 		return;
658 
659 	InvalidateItem(fCurrentPlaylistIndex);
660 	fCurrentPlaylistIndex = index;
661 	InvalidateItem(fCurrentPlaylistIndex);
662 }
663 
664 
665 void
_SetPlaybackState(uint32 state)666 PlaylistListView::_SetPlaybackState(uint32 state)
667 {
668 	if (fPlaybackState == state)
669 		return;
670 
671 	fPlaybackState = state;
672 	InvalidateItem(fCurrentPlaylistIndex);
673 }
674 
675 
676 void
_AddDropContextMenu()677 PlaylistListView::_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 
688 uint32
_ShowDropContextMenu(BPoint dropPoint)689 PlaylistListView::_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