Source code: com/virtuosotechnologies/asaph/maingui/SongListModel.java
1 /*
2 ================================================================================
3
4 FILE: SongListModel.java
5
6 PROJECT:
7
8 Asaph
9
10 CONTENTS:
11
12 ListModel implementation for a song list
13
14 PROGRAMMERS:
15
16 Daniel Azuma (DA) <dazuma@kagi.com>
17
18 COPYRIGHT:
19
20 Copyright (C) 2003 Daniel Azuma (dazuma@kagi.com)
21
22 This program is free software; you can redistribute it and/or
23 modify it under the terms of the GNU General Public License as
24 published by the Free Software Foundation; either version 2
25 of the License, or (at your option) any later version.
26
27 This program is distributed in the hope that it will be useful,
28 but WITHOUT ANY WARRANTY; without even the implied warranty of
29 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30 GNU General Public License for more details.
31
32 You should have received a copy of the GNU General Public
33 License along with this program; if not, write to
34 Free Software Foundation, Inc.
35 59 Temple Place, Suite 330
36 Boston, MA 02111-1307 USA
37
38 ================================================================================
39 */
40
41
42 package com.virtuosotechnologies.asaph.maingui;
43
44
45 import java.text.CollationKey;
46 import java.text.Collator;
47 import java.util.List;
48 import java.util.ArrayList;
49 import java.util.Collection;
50 import java.util.Collections;
51 import java.util.Comparator;
52 import java.util.Iterator;
53 import javax.swing.AbstractListModel;
54 import javax.swing.SwingUtilities;
55
56 import com.virtuosotechnologies.lib.base.LinkedObject;
57
58 import com.virtuosotechnologies.asaph.model.Song;
59 import com.virtuosotechnologies.asaph.model.SongID;
60 import com.virtuosotechnologies.asaph.model.SongDatabaseFailedException;
61 import com.virtuosotechnologies.asaph.model.SongDatabase;
62 import com.virtuosotechnologies.asaph.model.SongIDResultSet;
63 import com.virtuosotechnologies.asaph.model.opsemantics.GetFieldAsStringSemantics;
64 import com.virtuosotechnologies.asaph.model.opsemantics.PredicateSemantics;
65 import com.virtuosotechnologies.asaph.model.opsemantics.TruePredicateSemantics;
66 import com.virtuosotechnologies.asaph.model.opsemantics.PredicateFilterSemantics;
67 import com.virtuosotechnologies.asaph.modelutils.SongUtils;
68
69
70 /**
71 * ListModel implementation for a song list
72 */
73 /*package*/ class SongListModel
74 extends AbstractListModel
75 {
76 private static final String STR_SongList_SongNotFoundName =
77 ResourceAccess.Strings.buildString("SongList_SongNotFoundName");
78 private static final String STR_SongList_NoTitleName =
79 ResourceAccess.Strings.buildString("SongList_NoTitleName");
80 private static final String STR_SongList_DatabaseErrorName =
81 ResourceAccess.Strings.buildString("SongList_DatabaseErrorName");
82
83 /**
84 * Definition of ordering for songs
85 */
86 /*package*/ static abstract class ItemComparator
87 {
88 /**
89 * Compare two items.
90 */
91 /*package*/ abstract int compare(
92 Item item1,
93 Item item2);
94 }
95
96
97 /*package*/ static interface SizeChangeListener
98 {
99 public void sizeChanged(
100 SongListModel model);
101 }
102
103
104 private SongDatabase database_;
105 private SongUtils songUtils_;
106 private List list_;
107 private LinkedObject delim_;
108 private Collator collator_;
109 private ItemComparator curComparator_;
110 private PredicateSemantics filterPredicate_;
111 private SizeChangeListener sizeChangeListener_;
112 private int totalSize_;
113
114
115 /**
116 * JList element type
117 */
118 /*package*/ class Item
119 extends LinkedObject
120 {
121 private SongID songID_;
122 private String name_;
123 private CollationKey collationKey_;
124
125 private Item(
126 SongID songID)
127 {
128 collationKey_ = null;
129 songID_ = songID;
130 update();
131 }
132
133 private Item(
134 SongID songID,
135 String name)
136 {
137 collationKey_ = null;
138 songID_ = songID;
139 name_ = name;
140 }
141
142 private void update()
143 {
144 try
145 {
146 SongDatabase database = songID_.getDatabase();
147 SongIDResultSet resultSet = database.createEmptyResultSet();
148 resultSet.add(songID_);
149 database.performOperation(
150 new GetFieldAsStringSemantics.DefaultImplementation(
151 Song.MAINTITLE_FIELD, songUtils_),
152 resultSet);
153 SongIDResultSet.Entry entry = resultSet.getEntryFor(songID_);
154 if (entry.getData() != null)
155 {
156 setName((String)entry.getData());
157 }
158 else
159 {
160 setDatabaseErrorName();
161 }
162 }
163 catch (SongDatabaseFailedException ex)
164 {
165 setDatabaseErrorName();
166 }
167 }
168
169 private void setName(
170 String name)
171 {
172 if (name == null)
173 {
174 name_ = STR_SongList_SongNotFoundName;
175 }
176 else if (name.length() == 0)
177 {
178 name_ = STR_SongList_NoTitleName;
179 }
180 else
181 {
182 name_ = name;
183 }
184 collationKey_ = null;
185 }
186
187 private void setDatabaseErrorName()
188 {
189 name_ = STR_SongList_DatabaseErrorName;
190 }
191
192
193 /*package*/ SongID getSongID()
194 {
195 return songID_;
196 }
197
198 public String toString()
199 {
200 return name_;
201 }
202
203 public CollationKey getCollationKey()
204 {
205 if (collationKey_ == null)
206 {
207 collationKey_ = collator_.getCollationKey(name_);
208 }
209 return collationKey_;
210 }
211 }
212
213
214 /**
215 * Constructor
216 */
217 /*package*/ SongListModel(
218 SongDatabase database,
219 SongUtils songUtils,
220 ItemComparator comparator,
221 PredicateSemantics filter)
222 {
223 database_ = database;
224 songUtils_ = songUtils;
225 list_ = new ArrayList();
226 delim_ = new LinkedObject();
227 curComparator_ = comparator;
228 collator_ = Collator.getInstance();
229 collator_.setStrength(Collator.PRIMARY);
230 filterPredicate_ = filter;
231 totalSize_ = 0;
232 sizeChangeListener_ = null;
233 }
234
235
236 /**
237 * Required by ListModel interface
238 */
239 public Object getElementAt(
240 int index)
241 {
242 return list_.get(index);
243 }
244
245
246 /**
247 * Required by ListModel interface
248 */
249 public int getSize()
250 {
251 return list_.size();
252 }
253
254
255 /**
256 * Get the total size including filtered out elements
257 */
258 /*package*/ int getTotalSize()
259 {
260 return totalSize_;
261 }
262
263
264 /**
265 * Add a song id to the list model
266 */
267 /*package*/ int addSongID(
268 SongID songID)
269 {
270 assert songID.getDatabase().equals(database_);
271
272 Item item = new Item(songID);
273 item.linkThisBefore(delim_);
274 ++totalSize_;
275
276 int index = -1;
277 try
278 {
279 SongIDResultSet resultSet = database_.createEmptyResultSet();
280 resultSet.add(songID);
281 database_.performOperation(filterPredicate_, resultSet);
282 if (Boolean.TRUE.equals(resultSet.getEntryFor(songID).getData()))
283 {
284 if (curComparator_ == null)
285 {
286 list_.add(item);
287 index = list_.size()-1;
288 }
289 else
290 {
291 index = Collections.binarySearch(list_, item,
292 new Comparator()
293 {
294 public int compare(
295 Object o1,
296 Object o2)
297 {
298 return curComparator_.compare((Item)o1, (Item)o2);
299 }
300 });
301 if (index < 0)
302 {
303 index = -index-1;
304 }
305 list_.add(index, item);
306 }
307 fireIntervalAdded(this, index, index);
308 }
309 }
310 catch (SongDatabaseFailedException ex)
311 {
312 }
313 if (sizeChangeListener_ != null)
314 {
315 sizeChangeListener_.sizeChanged(this);
316 }
317 return index;
318 }
319
320
321 /**
322 * Add a collection of song ids to the list model
323 */
324 /*package*/ void addSongIDs(
325 Collection songIDs)
326 {
327 if (songIDs.isEmpty())
328 {
329 return;
330 }
331
332 SongIDResultSet resultSet = database_.createEmptyResultSet();
333
334 for (Iterator iter = songIDs.iterator(); iter.hasNext(); )
335 {
336 SongID songID = (SongID)iter.next();
337 assert songID.getDatabase().equals(database_);
338 resultSet.add(songID);
339 resultSet.getEntryFor(songID).setData(Boolean.FALSE);
340 }
341
342 try
343 {
344 database_.performOperation(filterPredicate_, resultSet);
345 }
346 catch (SongDatabaseFailedException ex)
347 {
348 }
349
350 int oldSize = list_.size();
351 for (Iterator iter = resultSet.getEntryCollection().iterator(); iter.hasNext(); )
352 {
353 SongIDResultSet.Entry entry = (SongIDResultSet.Entry)iter.next();
354 Item item = new Item(entry.getSongID());
355 item.linkThisBefore(delim_);
356 ++totalSize_;
357 if (Boolean.TRUE.equals(entry.getData()))
358 {
359 list_.add(item);
360 }
361 }
362
363 if (list_.size() > oldSize)
364 {
365 fireIntervalAdded(this, oldSize, list_.size()-1);
366 }
367 if (curComparator_ != null && !list_.isEmpty())
368 {
369 Collections.sort(list_,
370 new Comparator()
371 {
372 public int compare(
373 Object o1,
374 Object o2)
375 {
376 return curComparator_.compare((Item)o1, (Item)o2);
377 }
378 });
379 fireContentsChanged(this, 0, list_.size()-1);
380 }
381 if (sizeChangeListener_ != null)
382 {
383 sizeChangeListener_.sizeChanged(this);
384 }
385 }
386
387
388 /**
389 * Clear the whole list
390 */
391 /*package*/ void clear()
392 {
393 int s = list_.size();
394 list_.clear();
395 delim_.unlinkThis();
396 totalSize_ = 0;
397 if (s > 0)
398 {
399 fireIntervalRemoved(this, 0, s-1);
400 }
401 if (sizeChangeListener_ != null)
402 {
403 sizeChangeListener_.sizeChanged(this);
404 }
405 }
406
407
408 private int findSongIndex(
409 SongID songID)
410 {
411 for (int i=list_.size()-1; i>=0; --i)
412 {
413 Item info = (Item)list_.get(i);
414 if (info.getSongID().equals(songID))
415 {
416 return i;
417 }
418 }
419 return -1;
420 }
421
422
423 private Item findItem(
424 SongID songID)
425 {
426 for (LinkedObject elem = delim_.getNext(); elem != delim_; elem = elem.getNext())
427 {
428 Item item = (Item)elem;
429 if (item.getSongID().equals(songID))
430 {
431 return item;
432 }
433 }
434 return null;
435 }
436
437
438 /**
439 * Remove a song from the given list model
440 */
441 /*package*/ void removeSongID(
442 SongID songID)
443 {
444 int index = findSongIndex(songID);
445 if (index == -1)
446 {
447 Item item = findItem(songID);
448 if (item != null)
449 {
450 item.unlinkThis();
451 --totalSize_;
452 }
453 }
454 else
455 {
456 Item item = (Item)list_.remove(index);
457 item.unlinkThis();
458 --totalSize_;
459 fireIntervalRemoved(this, index, index);
460 }
461 if (sizeChangeListener_ != null)
462 {
463 sizeChangeListener_.sizeChanged(this);
464 }
465 }
466
467
468 /**
469 * Notify that a song name has been updated
470 */
471 /*package*/ int updateSongID(
472 SongID songID)
473 {
474 int index = findSongIndex(songID);
475 if (index == -1)
476 {
477 Item item = findItem(songID);
478 if (item != null)
479 {
480 item.update();
481 }
482 }
483 else
484 {
485 Item item = (Item)list_.get(index);
486 item.update();
487 if (curComparator_ != null &&
488 (index > 0 &&
489 curComparator_.compare((Item)list_.get(index-1), item) > 0) ||
490 (index < list_.size()-1 &&
491 curComparator_.compare(item, (Item)list_.get(index+1)) > 0))
492 {
493 list_.remove(index);
494 fireIntervalRemoved(this, index, index);
495 index = Collections.binarySearch(list_, item,
496 new Comparator()
497 {
498 public int compare(
499 Object o1,
500 Object o2)
501 {
502 return curComparator_.compare((Item)o1, (Item)o2);
503 }
504 });
505 if (index < 0)
506 {
507 index = -index-1;
508 }
509 list_.add(index, item);
510 fireIntervalAdded(this, index, index);
511 }
512 else
513 {
514 fireContentsChanged(this, index, index);
515 }
516 if (sizeChangeListener_ != null)
517 {
518 sizeChangeListener_.sizeChanged(this);
519 }
520 }
521 return index;
522 }
523
524
525 /*package*/ String getNameForSongID(
526 SongID id)
527 {
528 Item item = findItem(id);
529 if (item == null)
530 {
531 return null;
532 }
533 return item.name_;
534 }
535
536
537 /**
538 * Set the comparator used to order songs.
539 * A null comparator makes the ordering undefined.
540 */
541 /*package*/ void setComparator(
542 ItemComparator comparator)
543 {
544 if (!curComparator_.equals(comparator))
545 {
546 curComparator_ = comparator;
547 if (curComparator_ != null && !list_.isEmpty())
548 {
549 Collections.sort(list_,
550 new Comparator()
551 {
552 public int compare(
553 Object o1,
554 Object o2)
555 {
556 return curComparator_.compare((Item)o1, (Item)o2);
557 }
558 });
559 fireContentsChanged(this, 0, list_.size()-1);
560 }
561 }
562 }
563
564
565 /**
566 * Get the comparator currently used to order songs.
567 * A null comparator makes the ordering undefined.
568 */
569 /*package*/ ItemComparator getComparator()
570 {
571 return curComparator_;
572 }
573
574
575 /**
576 * This method is okay to be called from outside the AWT thread
577 */
578 /*package*/ void setFilter(
579 final PredicateSemantics filter)
580 {
581 try
582 {
583 SongIDResultSet resultSet = database_.performOperation(
584 new PredicateFilterSemantics.DefaultImplementation(filter), null);
585 final List newList = new ArrayList();
586 for (LinkedObject elem = delim_.getNext(); elem != delim_; elem = elem.getNext())
587 {
588 Item item = (Item)elem;
589 if (resultSet.contains(item.getSongID()))
590 {
591 newList.add(item);
592 }
593 }
594 if (curComparator_ != null)
595 {
596 Collections.sort(newList,
597 new Comparator()
598 {
599 public int compare(
600 Object o1,
601 Object o2)
602 {
603 return curComparator_.compare((Item)o1, (Item)o2);
604 }
605 });
606 }
607 SwingUtilities.invokeLater(
608 new Runnable()
609 {
610 public void run()
611 {
612 if (!list_.isEmpty())
613 {
614 fireIntervalRemoved(SongListModel.this, 0, list_.size()-1);
615 }
616 list_ = newList;
617 if (!list_.isEmpty())
618 {
619 fireIntervalAdded(SongListModel.this, 0, list_.size()-1);
620 }
621 filterPredicate_ = filter;
622 if (sizeChangeListener_ != null)
623 {
624 sizeChangeListener_.sizeChanged(SongListModel.this);
625 }
626 }
627 });
628 }
629 catch (SongDatabaseFailedException ex)
630 {
631 }
632 }
633
634
635 /*package*/ PredicateSemantics getFilter()
636 {
637 return filterPredicate_;
638 }
639
640
641 /*package*/ void setSizeChangeListener(
642 SizeChangeListener listener)
643 {
644 sizeChangeListener_ = listener;
645 }
646 }