Docjar: A Java Source and Docuemnt Enginecom.*    java.*    javax.*    org.*    all    new    plug-in

Quick Search    Search Deep

Source code: com/virtuosotechnologies/asaph/modelutils/SongUtilsImpl.java


1   /*
2   ================================================================================
3   
4     FILE:  SongUtilsImpl.java
5     
6     PROJECT:
7     
8       Asaph
9     
10    CONTENTS:
11    
12      Implementation of SongUtils API interface
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.modelutils;
43  
44  
45  import java.util.List;
46  import java.util.ArrayList;
47  import java.util.Map;
48  import java.util.HashMap;
49  import java.util.Iterator;
50  import javax.swing.event.UndoableEditListener;
51  import javax.swing.event.UndoableEditEvent;
52  import java.awt.datatransfer.Transferable;
53  
54  import com.virtuosotechnologies.lib.util.CollectingCompoundEdit;
55  
56  import com.virtuosotechnologies.asaph.model.Song;
57  import com.virtuosotechnologies.asaph.model.SimpleString;
58  import com.virtuosotechnologies.asaph.model.StringList;
59  import com.virtuosotechnologies.asaph.model.Field;
60  import com.virtuosotechnologies.asaph.model.StringField;
61  import com.virtuosotechnologies.asaph.model.StringListField;
62  import com.virtuosotechnologies.asaph.model.ChordSet;
63  import com.virtuosotechnologies.asaph.model.ChordSetKey;
64  import com.virtuosotechnologies.asaph.model.Variation;
65  import com.virtuosotechnologies.asaph.model.SongBlock;
66  import com.virtuosotechnologies.asaph.model.StandardSongBlock;
67  import com.virtuosotechnologies.asaph.model.AddedSongBlock;
68  import com.virtuosotechnologies.asaph.model.SongLine;
69  import com.virtuosotechnologies.asaph.model.SongLineMember;
70  import com.virtuosotechnologies.asaph.model.TextString;
71  import com.virtuosotechnologies.asaph.model.CommentString;
72  import com.virtuosotechnologies.asaph.model.ChordAnnotation;
73  import com.virtuosotechnologies.asaph.model.StringSongLineMember;
74  import com.virtuosotechnologies.asaph.model.notation.Note;
75  import com.virtuosotechnologies.asaph.model.notation.Interval;
76  import com.virtuosotechnologies.asaph.model.notation.Chord;
77  import com.virtuosotechnologies.asaph.model.notation.NotationFactory;
78  import com.virtuosotechnologies.asaph.notationmanager.NotationManager;
79  
80  
81  /**
82   * Implementation of SongUtils API interface
83   */
84  /*package*/ class SongUtilsImpl
85  implements SongUtils
86  {
87    private DataTransferUtilsImpl dataTransferUtils_;
88    private NotationManager notationManager_;
89    
90    
91    /**
92     * Constructor
93     */
94    /*package*/ SongUtilsImpl(
95      NotationManager notationManager)
96    {
97      notationManager_ = notationManager;
98    }
99    
100   
101   /*package*/ void setDataTransferUtils(
102     DataTransferUtilsImpl dataTransferUtils)
103   {
104     dataTransferUtils_ = dataTransferUtils;
105   }
106   
107   
108   /*package*/ Note fixNote(
109     Note note,
110     NotationFactory factory)
111   {
112     if (factory == note.getNotationFactory())
113     {
114       return note;
115     }
116     return factory.parseNote(note.generateString());
117   }
118   
119   
120   /*package*/ Chord fixChord(
121     Chord chord,
122     NotationFactory factory)
123   {
124     if (factory == chord.getNotationFactory())
125     {
126       return chord;
127     }
128     return factory.parseChord(chord.generateString());
129   }
130   
131   
132   /*package*/ Chord[] fixChords(
133     Chord[] chords,
134     NotationFactory factory)
135   {
136     if (chords.length > 0 && factory != chords[0].getNotationFactory())
137     {
138       for (int i=0; i<chords.length; ++i)
139       {
140         chords[i] = factory.parseChord(chords[i].generateString());
141       }
142     }
143     return chords;
144   }
145   
146   
147   /**
148    * Make one song a copy of another. Clears out the destination song, and copies
149    * the source into it. Does not affect the SongID or owner database of the destination.
150    * Also not undoable, so note that it breaks the undo chain.
151    *
152    * @param source source song
153    * @param destination destination song
154    */
155   public void copySong(
156     Song source,
157     Song destination)
158   {
159     // Clear destination
160     destination.clear(null);
161     
162     NotationFactory factory = notationManager_.getNotationFactoryForLocale(destination.getLocale());
163     
164     // Fields
165     for (Field field = source.getNextField(null); field != null;
166       field = source.getNextField(field))
167     {
168       if (field instanceof StringField)
169       {
170         destination.addStringField(field.getFieldName(),
171           ((StringField)field).getString(), null);
172       }
173       else
174       {
175         StringListField sslf = (StringListField)field;
176         StringListField dslf = destination.addStringListField(
177           sslf.getFieldName(), null);
178         for (SimpleString str = sslf.getNextString(null); str != null;
179           str = sslf.getNextString(str))
180         {
181           dslf.insertStringBefore(null, str.getString(), null);
182         }
183       }
184     }
185     
186     // ChordSets
187     Map csmap = new HashMap();
188     for (ChordSet scs = source.getNextChordSet(null); scs != null;
189       scs = source.getNextChordSet(scs))
190     {
191       ChordSet dcs = destination.addChordSet(scs.getKeySignatureType().getString(),
192         fixNote(scs.getNativeKey().getKeyNote(), factory), null);
193       csmap.put(scs, dcs);
194       dcs.getName().setString(scs.getName().getString(), null);
195       dcs.getNativeKey().getInfoString().setString(
196         scs.getNativeKey().getInfoString().getString(), null);
197       for (ChordSetKey sak = scs.getNextAlternateKey(null); sak != null;
198         sak = scs.getNextAlternateKey(sak))
199       {
200         ChordSetKey dak = dcs.addAlternateKey(fixNote(sak.getKeyNote(), factory), null);
201         dak.getInfoString().setString(sak.getInfoString().getString(), null);
202       }
203     }
204     destination.setDefaultChordSet(
205       (ChordSet)csmap.get(source.getDefaultChordSet()), null);
206     
207     // Variations
208     Map varmap = new HashMap();
209     for (Variation sv = source.getNextVariation(null); sv != null;
210       sv = source.getNextVariation(sv))
211     {
212       Variation dv = destination.addVariation(null);
213       varmap.put(sv, dv);
214       dv.getName().setString(sv.getName().getString(), null);
215     }
216     
217     // Blocks
218     for (SongBlock sb = source.getNextBlock(null); sb != null;
219       sb = source.getNextBlock(sb))
220     {
221       SongBlock db = null;
222       if (sb instanceof StandardSongBlock)
223       {
224         StandardSongBlock dsb = (StandardSongBlock)
225           destination.insertBlockBefore(null, null, null);
226         for (Iterator iter = ((StandardSongBlock)sb).getOmittingVariations().iterator();
227           iter.hasNext(); )
228         {
229           dsb.omitVariation((Variation)varmap.get(iter.next()), null);
230         }
231         db = dsb;
232       }
233       else if (sb instanceof AddedSongBlock)
234       {
235         db = destination.insertBlockBefore(null,
236           (Variation)varmap.get(((AddedSongBlock)sb).getAddingVariation()), null);
237       }
238       db.setIndentLevel(sb.getIndentLevel(), null);
239       for (SongLine sl = sb.getNextLine(null); sl != null;
240         sl = sb.getNextLine(sl))
241       {
242         SongLine dl = db.insertLineBefore(null, null);
243         dl.setIndentLevel(sl.getIndentLevel(), null);
244         for (SongLineMember sm = sl.getNextMember(null); sm != null;
245           sm = sl.getNextMember(sm))
246         {
247           if (sm instanceof TextString)
248           {
249             TextString sts = (TextString)sm;
250             TextString dts = dl.insertTextStringBefore(null,
251               sts.getString(), null);
252           }
253           else if (sm instanceof CommentString)
254           {
255             CommentString scs = (CommentString)sm;
256             CommentString dcs = dl.insertCommentStringBefore(null,
257               scs.getString(), null);
258           }
259           else if (sm instanceof ChordAnnotation)
260           {
261             ChordAnnotation sca = (ChordAnnotation)sm;
262             ChordAnnotation dca = dl.insertChordAnnotationBefore(null,
263               (ChordSet)csmap.get(sca.getChordSet()),
264               fixChord(sca.getPrimaryChord(), factory),
265               fixChords(sca.getPrecedingChords(), factory),
266               fixChords(sca.getFollowingChords(), factory), null);
267           }
268         }
269       }
270     }
271   }
272   
273   
274   /**
275    * Normalize a song for editing. Involves compacting each line and ensuring that
276    * the first and last song line member of each line is a text member.
277    * Not undoable, so note that it breaks the undo chain.
278    *
279    * @param song Song to prepare
280    * @return true if the song appears to have just been created, otherwise false.
281    */
282   public boolean prepareSongForEditing(
283     Song song)
284   {
285     boolean brandNew = (song.getBlockCount() == 0);
286     if (brandNew)
287     {
288       SongBlock block = song.insertBlockAfter(null, null, null);
289       SongLine line = block.insertLineAfter(null, null);
290       line.insertTextStringAfter(null, "", null);
291     }
292     else
293     {
294       for (SongBlock block = song.getNextBlock(null); block != null;
295         block = song.getNextBlock(block))
296       {
297         for (SongLine line = block.getNextLine(null); line != null;
298           line = block.getNextLine(line))
299         {
300           compactLine(line, null);
301         }
302       }
303     }
304     if (!(song.getNamedField(Song.MAINTITLE_FIELD) instanceof StringField))
305     {
306       song.addStringField(Song.MAINTITLE_FIELD, "", null);
307     }
308     if (!(song.getNamedField(Song.COMMENT_FIELD) instanceof StringField))
309     {
310       song.addStringField(Song.COMMENT_FIELD, "", null);
311     }
312     if (!(song.getNamedField(Song.AUTHOR_FIELD) instanceof StringField))
313     {
314       song.addStringField(Song.AUTHOR_FIELD, "", null);
315     }
316     if (!(song.getNamedField(Song.COPYRIGHT_FIELD) instanceof StringField))
317     {
318       song.addStringField(Song.COPYRIGHT_FIELD, "", null);
319     }
320     if (!(song.getNamedField(Song.ALTERNATETITLELIST_FIELD) instanceof StringListField))
321     {
322       song.addStringListField(Song.ALTERNATETITLELIST_FIELD, null);
323     }
324     if (!(song.getNamedField(Song.KEYWORDLIST_FIELD) instanceof StringListField))
325     {
326       song.addStringListField(Song.KEYWORDLIST_FIELD, null);
327     }
328     if (!(song.getNamedField(Song.NOTESLIST_FIELD) instanceof StringListField))
329     {
330       song.addStringListField(Song.NOTESLIST_FIELD, null);
331     }
332     return brandNew;
333   }
334   
335   
336   /**
337    * Compact a line-- aggregates any consecutive comment strings or text strings,
338    * ensures inserts empty text between adjacent chords in the same chord set, and
339    * ensures that the line starts and ends with strings.
340    * After this operation, any old handles to LineMembers may be invalid.
341    *
342    * @param line SongLine to compact
343    * @param undoListener listener to notify if an undoable edit is generated,
344    *     or null to suppress generation of undoable edits
345    * @exception NullPointerException line was null
346    */
347   public void compactLine(
348     SongLine line,
349     UndoableEditListener undoListener)
350   {
351     if (line == null)
352     {
353       throw new NullPointerException();
354     }
355     CollectingCompoundEdit editCollector = null;
356     if (undoListener != null)
357     {
358       editCollector = new CollectingCompoundEdit();
359     }
360     
361     TextString lastTextString = null;
362     CommentString lastCommentString = null;
363     for (SongLineMember member = line.getNextMember(null); member != null;
364       member = line.getNextMember(member))
365     {
366       if (member instanceof TextString)
367       {
368         if (lastTextString != null)
369         {
370           lastTextString.setString(
371             lastTextString.getString()+((TextString)member).getString(),
372             editCollector);
373           line.removeMember(member, editCollector);
374           member = lastTextString;
375         }
376         else
377         {
378           lastTextString = (TextString)member;
379         }
380         lastCommentString = null;
381       }
382       else if (member instanceof CommentString)
383       {
384         if (lastCommentString != null)
385         {
386           lastCommentString.setString(
387             lastCommentString.getString()+((CommentString)member).getString(),
388             editCollector);
389           line.removeMember(member, editCollector);
390           member = lastCommentString;
391         }
392         else
393         {
394           lastCommentString = (CommentString)member;
395         }
396         lastTextString = null;
397       }
398       else
399       {
400         ChordAnnotation annotation = (ChordAnnotation)member;
401         lastTextString = null;
402         lastCommentString = null;
403         SongLineMember prevMember = line.getPreviousMember(annotation,
404           annotation.getChordSet());
405         if (prevMember == null || prevMember instanceof ChordAnnotation)
406         {
407           line.insertTextStringBefore(annotation, "", editCollector);
408         }
409       }
410     }
411     if (!(line.getPreviousMember(null) instanceof StringSongLineMember))
412     {
413       line.insertTextStringBefore(null, "", editCollector);
414     }
415     
416     if (undoListener != null)
417     {
418       editCollector.end();
419       if (editCollector.isSignificant())
420       {
421         undoListener.undoableEditHappened(new UndoableEditEvent(this, editCollector));
422       }
423     }
424   }
425   
426   
427   /**
428    * Compact a song-- performs compactLine on all lines in the song.
429    * After this operation, any old handles to LineMembers may be invalid.
430    *
431    * @param song Song to compact
432    * @param undoListener listener to notify if an undoable edit is generated,
433    *     or null to suppress generation of undoable edits
434    * @exception NullPointerException line was null
435    */
436   public void compactSong(
437     Song song,
438     UndoableEditListener undoListener)
439   {
440     if (song == null)
441     {
442       throw new NullPointerException();
443     }
444     CollectingCompoundEdit editCollector = null;
445     if (undoListener != null)
446     {
447       editCollector = new CollectingCompoundEdit();
448     }
449     
450     for (SongBlock block = song.getNextBlock(null); block != null;
451       block = song.getNextBlock(block))
452     {
453       for (SongLine line = block.getNextLine(null); line != null;
454         line = block.getNextLine(line))
455       {
456         compactLine(line, editCollector);
457       }
458     }
459     
460     if (undoListener != null)
461     {
462       editCollector.end();
463       if (editCollector.isSignificant())
464       {
465         undoListener.undoableEditHappened(new UndoableEditEvent(this, editCollector));
466       }
467     }
468   }
469   
470   
471   /**
472    * Get a field value as a string.
473    * If the field is not present, returns the empty string.
474    * If the field is a string field, returns its value.
475    * If the field is a string list field, returns the values delimited by newlines.
476    *
477    * @param song song to query
478    * @param fieldName field name
479    * @return field value as a string
480    * @exception NullPointerException something was null
481    */
482   public String getFieldValueAsString(
483     Song song,
484     String fieldName)
485   {
486     Field field = song.getNamedField(fieldName);
487     if (field == null)
488     {
489       return "";
490     }
491     else if (field instanceof StringField)
492     {
493       return ((StringField)field).getString();
494     }
495     else
496     {
497       return getMultiLineString((StringListField)field);
498     }
499   }
500   
501   
502   /**
503    * Get a field value as a string array.
504    * If the field is not present, returns the zero-length array.
505    * If the field is a string field, returns a one-element array with the value.
506    * If the field is a string list field, returns the values in an array.
507    *
508    * @param song song to query
509    * @param fieldName field name
510    * @return field value as a string array
511    * @exception NullPointerException something was null
512    */
513   public String[] getFieldValueAsStringArray(
514     Song song,
515     String fieldName)
516   {
517     Field field = song.getNamedField(fieldName);
518     if (field == null)
519     {
520       return new String[0];
521     }
522     else if (field instanceof StringField)
523     {
524       return new String[]{((StringField)field).getString()};
525     }
526     else
527     {
528       StringListField listField = (StringListField)field;
529       String[] array = new String[listField.getStringCount()];
530       int index = 0;
531       for (SimpleString ss = listField.getNextString(null); ss != null;
532         ss = listField.getNextString(ss))
533       {
534         array[index] = ss.getString();
535         ++index;
536       }
537       return array;
538     }
539   }
540   
541   
542   /**
543    * Get the StringList value as a string with newline delimiters.
544    *
545    * @param stringList StringList to query
546    * @return field value as a string
547    * @exception NullPointerException something was null
548    */
549   public String getMultiLineString(
550     StringList stringList)
551   {
552     StringBuffer buf = new StringBuffer();
553     for (SimpleString ss = stringList.getNextString(null); ss != null;
554       ss = stringList.getNextString(ss))
555     {
556       buf.append(ss.getString());
557       buf.append("\n");
558     }
559     return new String(buf);
560   }
561   
562   
563   /**
564    * Parses the given string into lines and sets the string list value.
565    *
566    * @param stringList StringList to modify
567    * @param value field value as a string with newlines
568    * @param undoListener listener to notify if an undoable edit is generated,
569    *     or null to suppress generation of undoable edits
570    * @exception NullPointerException something was null
571    */
572   public void setMultiLineString(
573     StringList stringList,
574     String value,
575     UndoableEditListener undoListener)
576   {
577     CollectingCompoundEdit editCollector = null;
578     if (undoListener != null)
579     {
580       editCollector = new CollectingCompoundEdit();
581     }
582     
583     stringList.clear(editCollector);
584     char[] chars = value.toCharArray();
585     for (int start = 0; start < chars.length; )
586     {
587       int count = 0;
588       int next = start;
589       while (next < chars.length)
590       {
591         char ch = chars[next];
592         if (ch == '\r' || ch == '\n')
593         {
594           ++next;
595           if (ch == '\r' && next < chars.length && chars[next] == '\n')
596           {
597             ++next;
598           }
599           break;
600         }
601         else
602         {
603           ++next;
604           ++count;
605         }
606       }
607       if (count > 0 || next < chars.length)
608       {
609         stringList.insertStringBefore(null, new String(chars, start, count),
610           editCollector);
611       }
612       start = next;
613     }
614     
615     if (editCollector != null)
616     {
617       editCollector.end();
618       if (editCollector.isSignificant())
619       {
620         undoListener.undoableEditHappened(new UndoableEditEvent(this, editCollector));
621       }
622     }
623   }
624   
625   
626   /**
627    * Get the entire song text as a string. Comments are stripped. Lines are
628    * delimited by newlines, and blocks by double newlines.
629    *
630    * @param song song to query
631    * @param useVariation true to query a specific variation, specified by the variation
632    *    parameter, or false to query the entire text (which may not correspond to any
633    *    actual variation)
634    * @param variation variation to get, or null for the default variation. Ignored if
635    *    useVariation is false
636    * @return a string containing the song body text, sans comments
637    * @exception NullPointerException something was null
638    */
639   public String getSongTextAsString(
640     Song song,
641     boolean useVariation,
642     Variation variation)
643   {
644     StringBuffer songBuf = new StringBuffer();
645     
646     boolean first = true;
647     for (SongBlock block = (useVariation ? song.getNextBlock(null, variation) :
648         song.getNextBlock(null));
649       block != null;
650       block = (useVariation ? song.getNextBlock(block, variation) :
651         song.getNextBlock(block)))
652     {
653       StringBuffer blockBuf = new StringBuffer();
654       for (SongLine line = block.getNextLine(null); line != null;
655         line = block.getNextLine(line))
656       {
657         StringBuffer lineBuf = new StringBuffer();
658         for (SongLineMember member = line.getNextMember(null); member != null;
659           member = line.getNextMember(member))
660         {
661           if (member instanceof TextString)
662           {
663             lineBuf.append(((TextString)member).getString());
664           }
665         }
666         if (lineBuf.length() > 0)
667         {
668           blockBuf.append(lineBuf);
669           blockBuf.append('\n');
670         }
671       }
672       if (blockBuf.length() > 0)
673       {
674         if (first)
675         {
676           first = false;
677         }
678         else
679         {
680           songBuf.append('\n');
681         }
682         songBuf.append(blockBuf);
683       }
684     }
685     
686     return new String(songBuf);
687   }
688   
689   
690   /**
691    * Get the first line of the song as a string. Comments are stripped.
692    *
693    * @param song song to query
694    * @param useVariation true to query a specific variation, specified by the variation
695    *    parameter, or false to query the entire text (which may not correspond to any
696    *    actual variation)
697    * @param variation variation to get, or null for the default variation. Ignored if
698    *    useVariation is false
699    * @return a string containing the song body text, sans comments
700    * @exception NullPointerException something was null
701    */
702   public String getFirstLineAsString(
703     Song song,
704     boolean useVariation,
705     Variation variation)
706   {
707     for (SongBlock block = (useVariation ? song.getNextBlock(null, variation) :
708         song.getNextBlock(null));
709       block != null;
710       block = (useVariation ? song.getNextBlock(block, variation) :
711         song.getNextBlock(block)))
712     {
713       for (SongLine line = block.getNextLine(null); line != null;
714         line = block.getNextLine(line))
715       {
716         StringBuffer lineBuf = new StringBuffer();
717         for (SongLineMember member = line.getNextMember(null); member != null;
718           member = line.getNextMember(member))
719         {
720           if (member instanceof TextString)
721           {
722             lineBuf.append(((TextString)member).getString());
723           }
724         }
725         if (lineBuf.length() > 0)
726         {
727           return new String(lineBuf);
728         }
729       }
730     }
731     
732     return "";
733   }
734   
735   
736   /**
737    * Remove the given range from a line
738    *
739    * @param line line to edit
740    * @param startMember Start of the range to remove. Null for the beginning of the line
741    * @param startPos position within startMember
742    * @param endMember End of the range to remove. Null for the end of the line
743    * @param endPos position within endMember
744    * @param undoListener listener for undo records, or null to not generate edits
745    */
746   public void removeLineRange(
747     SongLine line,
748     StringSongLineMember startMember,
749     int startPos,
750     StringSongLineMember endMember,
751     int endPos,
752     UndoableEditListener undoListener)
753   {
754     CollectingCompoundEdit editCollector = null;
755     if (undoListener != null)
756     {
757       editCollector = new CollectingCompoundEdit();
758     }
759     
760     if (startMember != null && startMember.equals(endMember))
761     {
762       String str = startMember.getString();
763       startMember.setString(str.substring(0, startPos)+str.substring(endPos), editCollector);
764     }
765     else
766     {
767       SongLineMember memIter = startMember;
768       if (memIter == null)
769       {
770         memIter = line.getNextMember(null);
771       }
772       while (memIter != null)
773       {
774         SongLineMember next = line.getNextMember(memIter);
775         if (memIter.equals(startMember))
776         {
777           startMember.setString(startMember.getString().substring(0, startPos), editCollector);
778         }
779         else if (memIter.equals(endMember))
780         {
781           endMember.setString(endMember.getString().substring(endPos), editCollector);
782           next = null;
783         }
784         else
785         {
786           line.removeMember(memIter, editCollector);
787         }
788         memIter = next;
789       }
790       if (((startMember instanceof TextString) && (endMember instanceof TextString)) ||
791         ((startMember instanceof CommentString) && (endMember instanceof CommentString)))
792       {
793         startMember.setString(startMember.getString()+endMember.getString(), editCollector);
794         line.removeMember(endMember, editCollector);
795       }
796     }
797     
798     if (editCollector != null)
799     {
800       editCollector.end();
801       if (editCollector.isSignificant())
802       {
803         undoListener.undoableEditHappened(new UndoableEditEvent(this, editCollector));
804       }
805     }
806   }
807   
808   
809   /**
810    * Move a chord marking
811    *
812    * @param chord Chord to move. If null, inserts an empty chord.
813    * @param destinationMember Text member containing the chord destination
814    * @param destinationPos position within destinationMember
815    * @param destinationSet ChordSet to move chord to. If null, uses chord's set.
816    * @param undoListener listener for undo records, or null to not generate edits
817    * @return the new ChordAnnotation
818    */
819   public ChordAnnotation moveChordAnnotation(
820     ChordAnnotation chord,
821     StringSongLineMember destinationMember,
822     int destinationPos,
823     ChordSet destinationSet,
824     UndoableEditListener undoListener)
825   {
826     ChordAnnotation ret = null;
827     CollectingCompoundEdit editCollector = null;
828     if (undoListener != null)
829     {
830       editCollector = new CollectingCompoundEdit();
831     }
832     
833     SongLine line = destinationMember.getSongLine();
834     boolean isDestinationComment = (destinationMember instanceof CommentString);
835     NotationFactory factory = notationManager_.getNotationFactoryForLocale(line.getSong().getLocale());
836     
837     if (destinationSet == null && chord != null)
838     {
839       destinationSet = chord.getChordSet();
840     }
841     Chord primaryChord;
842     Chord[] preChords;
843     Chord[] postChords;
844     if (chord != null)
845     {
846       primaryChord = chord.getPrimaryChord();
847       preChords = chord.getPrecedingChords();
848       postChords = chord.getFollowingChords();
849     }
850     else
851     {
852       primaryChord = factory.getEmptyChord();
853       preChords = new Chord[0];
854       postChords = new Chord[0];
855     }
856     
857     String oldDestString = destinationMember.getString();
858     SongLineMember prevMember = null;
859     SongLineMember nextMember = null;
860     if (destinationPos <= 0)
861     {
862       prevMember = line.getPreviousMember(destinationMember, destinationSet);
863       if (chord != null && prevMember == chord)
864       {
865         return chord;
866       }
867     }
868     if (destinationPos >= oldDestString.length())
869     {
870       nextMember = line.getNextMember(destinationMember, destinationSet);
871       if (chord != null && nextMember == chord)
872       {
873         return chord;
874       }
875     }
876     
877     if (destinationPos <= 0 && destinationPos >= oldDestString.length())
878     {
879       if (prevMember instanceof StringSongLineMember)
880       {
881         ret = line.insertChordAnnotationBefore(destinationMember, destinationSet,
882           primaryChord, preChords, postChords, editCollector);
883       }
884       else if (nextMember instanceof StringSongLineMember)
885       {
886         ret = line.insertChordAnnotationAfter(destinationMember, destinationSet,
887           primaryChord, preChords, postChords, editCollector);
888       }
889       else
890       {
891         if (isDestinationComment)
892         {
893           line.insertCommentStringBefore(destinationMember, "", editCollector);
894         }
895         else
896         {
897           line.insertTextStringBefore(destinationMember, "", editCollector);
898         }
899         ret = line.insertChordAnnotationBefore(destinationMember, destinationSet,
900           primaryChord, preChords, postChords, editCollector);
901       }
902     }
903     else if (destinationPos <= 0)
904     {
905       if (!(prevMember instanceof StringSongLineMember))
906       {
907         if (isDestinationComment)
908         {
909           line.insertCommentStringBefore(destinationMember, "", editCollector);
910         }
911         else
912         {
913           line.insertTextStringBefore(destinationMember, "", editCollector);
914         }
915       }
916       ret = line.insertChordAnnotationBefore(destinationMember, destinationSet,
917         primaryChord, preChords, postChords, editCollector);
918     }
919     else if (destinationPos >= oldDestString.length())
920     {
921       if (!(nextMember instanceof StringSongLineMember))
922       {
923         if (isDestinationComment)
924         {
925           line.insertCommentStringAfter(destinationMember, "", editCollector);
926         }
927         else
928         {
929           line.insertTextStringAfter(destinationMember, "", editCollector);
930         }
931       }
932       ret = line.insertChordAnnotationAfter(destinationMember, destinationSet,
933         primaryChord, preChords, postChords, editCollector);
934     }
935     else
936     {
937       String str1 = oldDestString.substring(0, destinationPos);
938       String str2 = oldDestString.substring(destinationPos);
939       if (isDestinationComment)
940       {
941         line.insertCommentStringBefore(destinationMember, str1, editCollector);
942       }
943       else
944       {
945         line.insertTextStringBefore(destinationMember, str1, editCollector);
946       }
947       destinationMember.setString(str2, editCollector);
948       ret = line.insertChordAnnotationBefore(destinationMember, destinationSet,
949         primaryChord, preChords, postChords, editCollector);
950     }
951     
952     if (chord != null)
953     {
954       prevMember = line.getPreviousMember(chord);
955       nextMember = line.getNextMember(chord);
956       line.removeMember(chord, editCollector);
957       if (((prevMember instanceof TextString) && (nextMember instanceof TextString)) ||
958         ((prevMember instanceof CommentString) && (nextMember instanceof CommentString)))
959       {
960         StringSongLineMember prevString = (StringSongLineMember)prevMember;
961         StringSongLineMember nextString = (StringSongLineMember)nextMember;
962         prevString.setString(prevString.getString()+nextString.getString(), editCollector);
963         line.removeMember(nextString, editCollector);
964       }
965     }
966     
967     if (editCollector != null)
968     {
969       editCollector.end();
970       if (editCollector.isSignificant())
971       {
972         undoListener.undoableEditHappened(new UndoableEditEvent(this, editCollector));
973       }
974     }
975     
976     return ret;
977   }
978   
979   
980   /**
981    * Get the index of a line member within its line
982    *
983    * @param member member to query
984    * @param set ChordSet to limit to, or null for entire line
985    * @return position of member
986    */
987   public int getMemberPosition(
988     SongLineMember member,
989     ChordSet set)
990   {
991     if (set != null && member instanceof ChordAnnotation &&
992       !set.equals(((ChordAnnotation)member).getChordSet()))
993     {
994       return -1;
995     }
996     int ret = 0;
997     SongLine line = member.getSongLine();
998     if (set != null)
999     {
1000      for (SongLineMember iter = line.getPreviousMember(member, set);
1001        iter != null; iter = line.getPreviousMember(iter, set))
1002      {
1003        ++ret;
1004      }
1005    }
1006    else
1007    {
1008      for (SongLineMember iter = line.getPreviousMember(member);
1009        iter != null; iter = line.getPreviousMember(iter))
1010      {
1011        ++ret;
1012      }
1013    }
1014    return ret;
1015  }
1016  
1017  
1018  /**
1019   * Split a line in half.
1020   *
1021   * @param member member containing the split position
1022   * @param pos position within member
1023   * @param undoListener listener to notify if an undoable edit is generated,
1024   *     or null to suppress generation of undoable edits
1025   * @exception NullPointerException member was null
1026   */
1027  public SongLine splitLine(
1028    StringSongLineMember member,
1029    int pos,
1030    UndoableEditListener undoListener)
1031  {
1032    CollectingCompoundEdit editCollector = null;
1033    if (undoListener != null)
1034    {
1035      editCollector = new CollectingCompoundEdit();
1036    }
1037    
1038    SongLine firstLine = member.getSongLine();
1039    SongBlock block = firstLine.getSongBlock();
1040    Transferable transfer = dataTransferUtils_.createLineFragmentTransferable(
1041      firstLine, member, pos, null, 0, null, true, editCollector);
1042    
1043    SongLine secondLine = block.insertLineAfter(firstLine, editCollector);
1044    try
1045    {
1046      dataTransferUtils_.pasteTransferable(transfer,
1047        secondLine, null, 0, null, null, editCollector);
1048    }
1049    catch (CannotPasteException ex)
1050    {
1051      throw new RuntimeException("Unexpected");
1052    }
1053    
1054    if (undoListener != null)
1055    {
1056      editCollector.end();
1057      if (editCollector.isSignificant())
1058      {
1059        undoListener.undoableEditHappened(new UndoableEditEvent(this, editCollector));
1060      }
1061    }
1062    
1063    return block.getNextLine(firstLine);
1064  }
1065  
1066  
1067  /**
1068   * Split a block in half.
1069   *
1070   * @param block block to split.
1071   * @param line last line in the first half. The split position will be after this line.
1072   *     Pass null to "split" at the beginning of the block.
1073   * @param undoListener listener to notify if an undoable edit is generated,
1074   *     or null to suppress generation of undoable edits
1075   * @exception NullPointerException member was null
1076   */
1077  public SongBlock splitBlock(
1078    SongBlock block,
1079    SongLine line,
1080    UndoableEditListener undoListener)
1081  {
1082    CollectingCompoundEdit editCollector = null;
1083    if (undoListener != null)
1084    {
1085      editCollector = new CollectingCompoundEdit();
1086    }
1087    
1088    List linesToMove = new ArrayList();
1089    Song song = block.getSong();
1090    for (SongLine lineIter = block.getNextLine(line); lineIter != null;
1091      lineIter = block.getNextLine(lineIter))
1092    {
1093      linesToMove.add(lineIter);
1094    }
1095    Transferable transfer = dataTransferUtils_.createLineListTransferable(
1096      song, linesToMove, null, true, editCollector);
1097    
1098    Variation var = null;
1099    if (block instanceof AddedSongBlock)
1100    {
1101      var = ((AddedSongBlock)block).getAddingVariation();
1102    }
1103    SongBlock nblock = song.insertBlockAfter(block, var, editCollector);
1104    nblock.setIndentLevel(block.getIndentLevel(), editCollector);
1105    if (var == null)
1106    {
1107      StandardSongBlock firstBlock = (StandardSongBlock)block;
1108      StandardSongBlock secondBlock = (StandardSongBlock)song.getNextBlock(firstBlock);
1109      for (Iterator iter = firstBlock.getOmittingVariations().iterator(); iter.hasNext(); )
1110      {
1111        secondBlock.omitVariation((Variation)iter.next(), editCollector);
1112      }
1113    }
1114    
1115    try
1116    {
1117      dataTransferUtils_.pasteTransferableAfter(transfer,
1118        nblock, null, null, var, editCollector);
1119    }
1120    catch (CannotPasteException ex)
1121    {
1122      throw new RuntimeException("Unexpected");
1123    }
1124    
1125    if (undoListener != null)
1126    {
1127      editCollector.end();
1128      if (editCollector.isSignificant())
1129      {
1130        undoListener.undoableEditHappened(new UndoableEditEvent(this, editCollector));
1131      }
1132    }
1133    
1134    return nblock;
1135  }
1136  
1137  
1138  private void internalTransposeLine(
1139    SongLine line,
1140    ChordSet chordSet,
1141    Interval interval,
1142    UndoableEditListener undoListener)
1143  {
1144    for (SongLineMember member = line.getNextMember(null); member != null;
1145      member = line.getNextMember(member))
1146    {
1147      if (member instanceof ChordAnnotation)
1148      {
1149        ChordAnnotation annotation = (ChordAnnotation)member;
1150        if (annotation.isInChordSet(chordSet))
1151        {
1152          Chord main = annotation.getPrimaryChord();
1153          Chord[] pre = annotation.getPrecedingChords();
1154          Chord[] post = annotation.getFollowingChords();
1155          main = main.getRaised(interval);
1156          for (int i=0; i<pre.length; ++i)
1157          {
1158            pre[i] = pre[i].getRaised(interval);
1159          }
1160          for (int i=0; i<post.length; ++i)
1161          {
1162            post[i] = post[i].getRaised(interval);
1163          }
1164          annotation.setPrimaryChord(main, undoListener);
1165          annotation.setPrecedingChords(pre, undoListener);
1166          annotation.setFollowingChords(post, undoListener);
1167        }
1168      }
1169    }
1170  }
1171  
1172  
1173  /**
1174   * Transpose chords in the given song line.
1175   *
1176   * @param line line to transpose
1177   * @param chordSet chord set to transpose
1178   * @param interval interval to transpose
1179   * @param undoListener listener to notify if an undoable edit is generated,
1180   *     or null to suppress generation of undoable edits
1181   * @exception NullPointerException something was null
1182   */
1183  public void transpose(
1184    SongLine line,
1185    ChordSet chordSet,
1186    Interval interval,
1187    UndoableEditListener undoListener)
1188  {
1189    CollectingCompoundEdit editCollector = null;
1190    if (undoListener != null)
1191    {
1192      editCollector = new CollectingCompoundEdit();
1193    }
1194    
1195    internalTransposeLine(line, chordSet, interval, editCollector);
1196    
1197    if (undoListener != null)
1198    {
1199      editCollector.end();
1200      if (editCollector.isSignificant())
1201      {
1202        undoListener.undoableEditHappened(new UndoableEditEvent(this, editCollector));
1203      }
1204    }
1205  }
1206  
1207  
1208  /**
1209   * Transpose chords in the given song block.
1210   *
1211   * @param block block to transpose
1212   * @param chordSet chord set to transpose
1213   * @param interval interval to transpose
1214   * @param undoListener listener to notify if an undoable edit is generated,
1215   *     or null to suppress generation of undoable edits
1216   * @exception NullPointerException something was null
1217   */
1218  public void transpose(
1219    SongBlock block,
1220    ChordSet chordSet,
1221    Interval interval,
1222    UndoableEditListener undoListener)
1223  {
1224    CollectingCompoundEdit editCollector = null;
1225    if (undoListener != null)
1226    {
1227      editCollector = new CollectingCompoundEdit();
1228    }
1229    
1230    for (SongLine line = block.getNextLine(null); line != null;
1231      line = block.getNextLine(line))
1232    {
1233      internalTransposeLine(line, chordSet, interval, editCollector);
1234    }
1235    
1236    if (undoListener != null)
1237    {
1238      editCollector.end();
1239      if (editCollector.isSignificant())
1240      {
1241        undoListener.undoableEditHappened(new UndoableEditEvent(this, editCollector));
1242      }
1243    }
1244  }
1245  
1246  
1247  /**
1248   * Transpose chords in the given song.
1249   *
1250   * @param song song to transpose
1251   * @param chordSet chord set to transpose
1252   * @param interval interval to transpose
1253   * @param undoListener listener to notify if an undoable edit is generated,
1254   *     or null to suppress generation of undoable edits
1255   * @exception NullPointerException something was null
1256   */
1257  public void transpose(
1258    Song song,
1259    ChordSet chordSet,
1260    Interval interval,
1261    UndoableEditListener undoListener)
1262  {
1263    CollectingCompoundEdit editCollector = null;
1264    if (undoListener != null)
1265    {
1266      editCollector = new CollectingCompoundEdit();
1267    }
1268    
1269    for (SongBlock block = song.getNextBlock(null); block != null;
1270      block = song.getNextBlock(block))
1271    {
1272      for (SongLine line = block.getNextLine(null); line != null;
1273        line = block.getNextLine(line))
1274      {
1275        internalTransposeLine(line, chordSet, interval, editCollector);
1276      }
1277    }
1278    
1279    if (undoListener != null)
1280    {
1281      editCollector.end();
1282      if (editCollector.isSignificant())
1283      {
1284        undoListener.undoableEditHappened(new UndoableEditEvent(this, editCollector));
1285      }
1286    }
1287  }
1288}