1/*
2   Project: MPDCon
3
4   Copyright (C) 2004
5
6   Author: Daniel Luederwald
7
8   Created: 2004-05-12 17:59:14 +0200 by flip
9
10   Playlist Controller
11
12   This application is free software; you can redistribute it and/or
13   modify it under the terms of the GNU General Public
14   License as published by the Free Software Foundation; either
15   version 2 of the License, or (at your option) any later version.
16
17   This application is distributed in the hope that it will be useful,
18   but WITHOUT ANY WARRANTY; without even the implied warranty of
19   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
20   Library General Public License for more details.
21
22   You should have received a copy of the GNU General Public
23   License along with this library; if not, write to the Free
24   Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111 USA.
25*/
26
27#include <AppKit/AppKit.h>
28#include "PlaylistController.h"
29#include "SongRatingCell.h"
30
31/* ---------------------
32   - Private Interface -
33   ---------------------*/
34@interface PlaylistController(Private)
35- (void) _doRemove;
36- (void) _moveRows: (NSArray *)rows atRow: (NSInteger)row;
37- (void) _addSongs: (NSArray *)paths atRow: (NSInteger)row;
38- (NSString *) _playlistLength;
39
40- (void) _filterListByString: (NSString *)filterString;
41@end
42
43@implementation PlaylistController
44
45/* --------------------------
46   - Initialization Methods -
47   --------------------------*/
48
49+ (id) sharedPlaylistController
50{
51  static PlaylistController *_sharedPlaylistController = nil;
52
53  if (! _sharedPlaylistController)
54    {
55    _sharedPlaylistController = [[PlaylistController allocWithZone:
56						       [self zone]] init];
57    }
58
59  return _sharedPlaylistController;
60}
61
62- (id) init
63{
64
65  if ((self = [self initWithWindowNibName: @"PlaylistViewer"]) != nil)
66    {
67      [self setWindowFrameAutosaveName: @"PlaylistViewer"];
68      mpdController = [MPDController sharedMPDController];
69      [playlist autorelease];
70      playlist = [[mpdController getPlaylist] retain];
71    }
72  return self;
73}
74
75- (void) dealloc
76{
77  [playlist release];
78  [playlistTimes release];
79  [ratingCol release];
80
81  [super dealloc];
82}
83
84/* ---------------
85   - Gui Methods -
86   ---------------*/
87
88- (void) awakeFromNib
89{
90  NSNotificationCenter *defCenter;
91
92  SongRatingCell *ratingCell;
93
94  ASSIGN(ratingCol, [playlistTable tableColumnWithIdentifier: @"rating"]);
95
96  [playlistTable setAutosaveName: @"PlaylistTable"];
97  [playlistTable setAutosaveTableColumns: YES];
98  [playlistTable setTarget: self];
99  [playlistTable setDoubleAction: @selector(doubleClicked:)];
100
101  [playlistTable registerForDraggedTypes:
102		   [NSArray arrayWithObjects: PlaylistDragType, CollectionDragType, nil]];
103
104  [lengthView setFont: [NSFont systemFontOfSize: 12]];
105
106  defCenter = [NSNotificationCenter defaultCenter];
107
108  [defCenter addObserver: self
109		selector: @selector(songChanged:)
110	            name: SongChangedNotification
111	          object: nil];
112
113  [defCenter addObserver: self
114	        selector: @selector(playlistChanged:)
115	            name: PlaylistChangedNotification
116	          object: nil];
117
118  [defCenter addObserver: self
119	        selector: @selector(didNotConnect:)
120	            name: DidNotConnectNotification
121	          object: nil];
122
123  currentSong = [mpdController getCurrentSongNr];
124
125  ratingCell = [[SongRatingCell alloc] init];
126  [ratingCol setDataCell:ratingCell];
127
128  [self playlistChanged: nil];
129  RELEASE(ratingCell);
130}
131
132- (void) removeSongs: (id)sender
133{
134  switch ([removeSelector indexOfSelectedItem])
135    {
136    case 1:
137      [mpdController clearPlaylist];
138      break;
139    case 2:
140      [self _doRemove];
141      break;
142    }
143}
144
145- (void) managePlaylists: (id)sender
146{
147  [[PlaylistsManagerController sharedPLManagerController] showWindow: self];
148}
149
150- (void) doubleClicked: (id)sender
151{
152  int row;
153
154  if ([playlistTable clickedRow] == -1)
155    {
156      return;
157    }
158
159  row = [playlistTable selectedRow];
160  [mpdController playSong: [[playlist objectAtIndex: row] getPos]];
161}
162
163- (void) showCurrentSong: (id)sender
164{
165  int state;
166
167  state = [mpdController getState];
168  if ((state == state_PAUSE) || (state == state_PLAY))
169    {
170      if (([playlist count] >= currentSong) &&
171          ([[playlist objectAtIndex: currentSong -1] getPos] == currentSong -1))
172        {
173          [playlistTable scrollRowToVisible: currentSong-1];
174          [playlistTable selectRow: currentSong-1 byExtendingSelection: NO];
175        }
176      else
177        {
178          NSBeep();
179        }
180    }
181}
182
183- (void) shuffleList: (id)sender
184{
185  [mpdController shuffleList];
186}
187
188- (void) browseCollection: (id)sender
189{
190  [[CollectionController sharedCollectionController] showWindow: self];
191}
192
193- (void) filterList: (id)sender
194{
195  int selectedRow;
196  int selectedPos = -1;
197
198  selectedRow = [playlistTable selectedRow];
199
200  if (selectedRow != -1)
201    {
202      selectedPos = [[playlist objectAtIndex: selectedRow] getPos];
203    }
204
205  [self playlistChanged: nil];
206
207  if (selectedPos != -1)
208    {
209      int i;
210
211      for (i = 0; i < [playlist count]; i++)
212        {
213          if ([[playlist objectAtIndex: i] getPos] == selectedPos)
214            {
215              [playlistTable scrollRowToVisible: i];
216              [playlistTable selectRow: i byExtendingSelection: NO];
217              break;
218            }
219        }
220    }
221}
222
223- (void) clearFilter: (id)sender
224{
225  [filterField setStringValue: @""];
226
227  [self filterList: sender];
228
229}
230
231/* ----------------------------
232   - some convenience Methods -
233   ---------------------------- */
234
235- (NSString *) playlistLength
236{
237  return [self _playlistLength];
238}
239
240/* --------------------------------
241   - TableView dataSource Methods -
242   --------------------------------*/
243
244- (NSInteger) numberOfRowsInTableView: (NSTableView *)tableView
245{
246    return [playlist count];
247}
248
249-          (id) tableView: (NSTableView *)tableView
250objectValueForTableColumn: (NSTableColumn *)tableColumn row:(NSInteger)row
251{
252  NSString *identifier;
253
254  identifier = [tableColumn identifier];
255
256   if ([identifier isEqual: @"pos"])
257     {
258       return [NSString stringWithFormat: @"%d", [[playlist objectAtIndex: row] getPos]+1];
259     }
260   else if ([identifier isEqual: @"time"])
261     {
262       return [playlistTimes objectAtIndex: row];
263     }
264   else if ([identifier isEqual: @"play"])
265     {
266       if (([[playlist objectAtIndex: row] getPos] == currentSong-1))
267	 {
268       int mState = [mpdController getState];
269
270       if ((mState == state_PAUSE) || (mState == state_PLAY))
271         {
272    	   return [NSImage imageNamed: @"Current.tiff"];
273         }
274       else
275         {
276           return nil;
277         }
278	 }
279       else
280	 {
281       return nil;
282	 }
283     }
284   else
285     {
286       return [[playlist objectAtIndex: row] valueForKey: identifier];
287     }
288}
289
290
291/* ------------------------------
292   - TableView dragging Methods -
293   ------------------------------*/
294
295- (NSDragOperation) tableView: (NSTableView *)tv
296		 validateDrop: (id <NSDraggingInfo>)info
297		  proposedRow: (NSInteger)row
298	proposedDropOperation: (NSTableViewDropOperation)dropOperation
299{
300  NSArray *typeArray;
301  NSString *availableType;
302  NSPasteboard *pboard;
303  NSDragOperation dragOperation;
304
305  pboard = [info draggingPasteboard];
306  typeArray = [NSArray arrayWithObjects: PlaylistDragType, CollectionDragType, nil];
307  availableType = [pboard availableTypeFromArray: typeArray];
308  dragOperation = NSDragOperationNone;
309
310  if (availableType)
311    {
312      [tv setDropRow: row dropOperation: NSTableViewDropAbove];
313      if ([[filterField stringValue] compare: @""] == NSOrderedSame)
314        {
315          if (([availableType isEqualToString: PlaylistDragType]) ||
316             ([availableType isEqualToString: CollectionDragType]))
317             {
318               dragOperation = NSDragOperationMove;
319             }
320        }
321
322    }
323  return dragOperation;
324}
325
326- (BOOL) tableView: (NSTableView *)tv
327	acceptDrop: (id <NSDraggingInfo>)info
328	       row: (NSInteger)row
329     dropOperation: (NSTableViewDropOperation)dropOperation
330{
331  NSPasteboard *pboard;
332  NSArray *objectsList, *typeArray;
333  NSString *availableType;
334  BOOL accept;
335
336  pboard = [info draggingPasteboard];
337  typeArray = [NSArray arrayWithObjects: PlaylistDragType, CollectionDragType, nil];
338
339  availableType = [pboard availableTypeFromArray: typeArray];
340  objectsList = [pboard propertyListForType: availableType];
341
342  if ([objectsList count])
343    {
344      if ([availableType isEqualToString: PlaylistDragType])
345	{
346	  [self _moveRows: objectsList atRow: row];
347      [tv deselectAll: nil];
348	}
349      else if ([availableType isEqualToString: CollectionDragType])
350        {
351          [self _addSongs: objectsList atRow: row];
352        }
353      accept = YES;
354    }
355  else
356    {
357      accept = NO;
358    }
359
360  return accept;
361}
362
363
364- (BOOL) tableView: (NSTableView *)tv
365	 writeRows: (NSArray *)rows
366      toPasteboard: (NSPasteboard*)pboard
367{
368  NSArray *typeArray;
369  BOOL accept;
370  unsigned int count;
371
372  count = [rows count];
373
374  if (count > 0)
375    {
376      accept = YES;
377      typeArray = [NSArray arrayWithObjects: PlaylistDragType, nil];
378      [pboard declareTypes: typeArray owner: self];
379      [pboard setPropertyList: rows forType: PlaylistDragType];
380    }
381  else
382    {
383      accept = NO;
384    }
385
386  return accept;
387}
388
389/* ------------------------
390   - Notification Methods -
391   ------------------------*/
392
393- (void) songChanged: (NSNotification *)aNotif
394{
395  currentSong = [mpdController getCurrentSongNr];
396  [playlistTable reloadData];
397}
398
399- (void) playlistChanged: (NSNotification *)aNotif
400{
401  int j;
402
403  [playlist release];
404
405  playlist = [[mpdController getPlaylist] retain];
406
407  if ([[filterField stringValue] compare: @""] != NSOrderedSame)
408    {
409      [self _filterListByString: [filterField stringValue]];
410    }
411
412  [lengthView setStringValue: [NSString stringWithFormat:
413	 _(@"Playlist Length: %@"), [self _playlistLength]]];
414
415  [playlistTimes autorelease];
416
417  playlistTimes = [[NSMutableArray alloc] init];
418
419  for (j = 0; j < [playlist count]; j++)
420    {
421      int time;
422      int tSecs, tMins;
423
424      time = [[playlist objectAtIndex: j] getTotalTime];
425
426      tSecs = (time % 60);
427      tMins = (int) time/60;
428
429    [playlistTimes addObject: [NSString stringWithFormat: @"%d:%02d",
430					tMins, tSecs]];
431    }
432
433  [playlistTable reloadData];
434  [playlistTable deselectAll: nil];
435  currentSong = [mpdController getCurrentSongNr];
436  [self showCurrentSong: nil];
437}
438
439
440- (void) didNotConnect: (NSNotification *)aNotif
441{
442  [[self window] performClose: self];
443}
444
445@end
446
447/* -------------------
448   - Private Methods -
449   -------------------*/
450
451@implementation PlaylistController(Private)
452- (NSString *)_playlistLength
453{
454  int length = 0;
455  if ([playlist count] != 0)
456    {
457      int i;
458      int tSecs, tMins, tHours;
459
460      for (i = 0; i < [playlist count]; i++)
461        {
462          length += [[playlist objectAtIndex: i] getTotalTime];
463        }
464
465    tSecs = (length % 60);
466    tMins = (int) (length/60) % 60;
467    tHours = (int) length/3600;
468
469    return [NSString stringWithFormat: _(@"%d:%02d:%02d"),
470                    tHours, tMins, tSecs];
471    }
472  else
473    {
474      return @"0:00:00";
475    }
476}
477
478- (void) _doRemove
479{
480  NSIndexSet *selectedRows;
481  NSInteger i;
482
483  if ([playlistTable numberOfSelectedRows] == [mpdController getPlaylistLength])
484    {
485      [mpdController clearPlaylist];
486      return;
487    }
488  selectedRows = [playlistTable selectedRowIndexes];
489  for (i=[selectedRows lastIndex];i != NSNotFound;i = [selectedRows indexLessThanIndex: i])
490    {
491      [mpdController removeSong: [[playlist objectAtIndex: i] getPos]];
492    }
493  [playlistTable deselectAll: self];
494}
495
496
497- (void) _moveRows: (NSArray *)rows atRow: (NSInteger)row
498{
499  NSUInteger i;
500
501  NSUInteger count = [rows count];
502
503  NSUInteger ids[count];
504
505  for (i = 0; i < count; i++) {
506      ids[i] = [[playlist objectAtIndex: [[rows objectAtIndex: i] intValue]] getID];
507  }
508
509  for (i = 0; i < count; i++) {
510      if ([[rows objectAtIndex: 0] intValue] < row)
511    	  [mpdController moveSongWithID: ids[i] to: row-1];
512      else
513       	  [mpdController moveSongWithID: ids[i] to: row+i];
514  }
515
516  [playlistTable reloadData];
517}
518
519- (void) _addSongs: (NSArray *)songs atRow: (NSInteger)row
520{
521  NSUInteger i;
522
523  for (i = 0; i < [songs count]; i++) {
524      [mpdController addTrack: [songs objectAtIndex: i]];
525      [mpdController moveSongNr: [mpdController getPlaylistLength]-1 to: (row+i)];
526  }
527}
528
529- (void) _filterListByString: (NSString *) fString
530{
531  NSMutableArray *tmpArray;
532  NSUInteger i;
533
534  tmpArray = [[NSMutableArray alloc] init];
535
536  for (i = 0; i < [playlist count]; i++) {
537      if ([[[playlist objectAtIndex: i] getArtist] rangeOfString: fString options: NSCaseInsensitiveSearch].location != NSNotFound)  {
538          [tmpArray addObject: [playlist objectAtIndex: i]];
539      } else if ([[[playlist objectAtIndex: i] getTitle] rangeOfString: fString options: NSCaseInsensitiveSearch].location != NSNotFound)  {
540          [tmpArray addObject: [playlist objectAtIndex: i]];
541      } else if ([[[playlist objectAtIndex: i] getAlbum] rangeOfString: fString options: NSCaseInsensitiveSearch].location != NSNotFound)  {
542          [tmpArray addObject: [playlist objectAtIndex: i]];
543      } else if ([[[playlist objectAtIndex: i] getGenre] rangeOfString: fString options: NSCaseInsensitiveSearch].location != NSNotFound)  {
544          [tmpArray addObject: [playlist objectAtIndex: i]];
545      }
546  }
547
548  [playlist release];
549
550  playlist = tmpArray;
551}
552
553-(void) tableView: (NSTableView*) aTableView
554   setObjectValue: (id) anObj
555   forTableColumn: (NSTableColumn*) aTableColumn
556              row: (NSInteger) rowIndex
557{
558    if (aTableColumn == ratingCol) {
559        id playlistItem;
560
561        /* We can't keep that as an assertion now, as it can easily fail when
562         * the broken GNUstep NSTableView lets you edit the string value for the cell.
563         */
564        if ([anObj isKindOfClass: [NSNumber class]] == NO) {
565            NSLog(@"Warning: %@ is not a number value.", anObj);
566        }
567
568        playlistItem = [playlist objectAtIndex: rowIndex];
569        [playlistItem setRating: [anObj integerValue]];
570    }
571}
572
573@end
574