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}