package com.xenoage.zong.io.selection; import static com.xenoage.utils.CheckUtils.checkArgsNotNull; import static com.xenoage.utils.math.Fraction._0; import static com.xenoage.zong.core.music.beam.Beam.beam; import static com.xenoage.zong.core.position.MP.mp; import java.util.ArrayList; import lombok.Getter; import lombok.Setter; import com.xenoage.utils.document.command.CommandPerformer; import com.xenoage.utils.math.Fraction; import com.xenoage.zong.commands.core.music.ColumnElementWrite; import com.xenoage.zong.commands.core.music.MeasureAdd; import com.xenoage.zong.commands.core.music.MeasureElementWrite; import com.xenoage.zong.commands.core.music.StaffAdd; import com.xenoage.zong.commands.core.music.VoiceAdd; import com.xenoage.zong.commands.core.music.VoiceElementWrite; import com.xenoage.zong.commands.core.music.slur.SlurAdd; import com.xenoage.zong.core.Score; import com.xenoage.zong.core.music.ColumnElement; import com.xenoage.zong.core.music.Measure; import com.xenoage.zong.core.music.MeasureElement; import com.xenoage.zong.core.music.Pitch; import com.xenoage.zong.core.music.VoiceElement; import com.xenoage.zong.core.music.beam.Beam; import com.xenoage.zong.core.music.beam.BeamWaypoint; import com.xenoage.zong.core.music.chord.Chord; import com.xenoage.zong.core.music.chord.Note; import com.xenoage.zong.core.music.slur.Slur; import com.xenoage.zong.core.music.slur.SlurType; import com.xenoage.zong.core.music.slur.SlurWaypoint; import com.xenoage.zong.core.music.util.MPE; import com.xenoage.zong.core.position.MP; import com.xenoage.zong.io.score.ScoreInputOptions; import com.xenoage.zong.io.score.ScoreInputOptions.WriteMode; import com.xenoage.zong.utils.exceptions.IllegalMPException; import com.xenoage.zong.utils.exceptions.MeasureFullException; /** * Cursor for a score. * * This is the most often used selection, since it is also useful for * sequential input like needed when reading from a file. * The actions can not be undone. If undo is needed, execute commands * on the {@link CommandPerformer} from the {@link Score} class. * * It contains the current position within the score and provides many methods * to write or remove elements and move the cursor. * * If the {@link #moving} flag is set, the cursor jumps to the end of written elements * instead of staying at its old position. Then it can be at a {@link MP} which * still does not exist (e.g. at the end of the score), which isn't a problem * since it is created as soon as needed. * * There is also the possibility to open and close beams and curved lines. * * @author Andreas Wenger * @author Uli Teschemacher */ public final class Cursor implements ScoreSelection { /** The score which will be modified. */ @Getter private final Score score; /** The current position of the cursor. It contains both an element index and a beat. */ private MP mp; /** True, when the cursor is moved when the write method is executed. */ @Getter @Setter private boolean moving; /** True, when empty space should be filled with invisible rests. Defaults to false. */ @Getter @Setter private boolean fillWithHiddenRests = false; private ArrayList<BeamWaypoint> openBeamWaypoints = null; private ArrayList<SlurWaypoint> openSlurWaypoints = null; private SlurType openSlurType = null; /** * Creates a new {@link Cursor}. * @param score the score to work on * @param pos the musical position of the cursor, which may still not exist * in the score (the measure and voice will be created on demand) * @param moving move with input? */ public Cursor(Score score, MP mp, boolean moving) { this.score = score; this.mp = mp; if (this.mp.beat == MP.unknownBeat) this.mp = this.mp.getWithBeat(score); this.moving = moving; } /** * The current position of the cursor. It contains both an element index and a beat. */ @Override public MP getMP() { return mp; } /** * Sets the position of the cursor. The {@link MP#element} field must be set. * @deprecated new naming: setMp */ public void setMP(MP mp) { setMp(mp); } /** * Sets the position of the cursor. The {@link MP#element} field must be set. */ public void setMp(MP mp) { if (mp.element == MP.unknown) throw new IllegalArgumentException("unknown element"); if (mp.beat == MP.unknownBeat) { //beat is unknown. compute it or use 0 for nonexisting MPs. if (score.isMPExisting(mp)) mp = mp.getWithBeat(score); else mp = mp.withBeat(_0); } this.mp = mp; } /** * Writes the given {@link ColumnElement} at the current position. * Dependent on its type, it may replace elements of the same type. */ @Override public void write(ColumnElement element) { ensureMeasureExists(mp); new ColumnElementWrite(element, score.getColumnHeader(mp.measure), mp.beat, null).execute(); } /** * See {@link #write(ColumnElement)}. */ @Override public void write(ColumnElement element, ScoreInputOptions options) { write(element); } /** * Writes the given pitches as a chord. The position and overwrite mode * depends on the given options. */ @Override public void write(Pitch[] pitches, ScoreInputOptions options) throws IllegalMPException, MeasureFullException { WriteMode wm = options.getWriteMode(); if (wm == WriteMode.ChordBeforeCursor) { //add the given pitches to the chord before the cursor addPitchesToPrecedingChord(pitches); } else { //default behaviour: write chord after cursor Chord chord = new Chord(Note.notes(pitches), options.getDuration()); write(chord, options); } } /** * Adds the given pitches to the chord before the cursor. If there is no * chord, nothing is done. */ private void addPitchesToPrecedingChord(Pitch... pitches) { //find the last voice element starting before the current position MPE<VoiceElement> ive = score.getStaff(mp.staff).getVoiceElementBefore(mp, false); //if the target element is a chord, add the given pitches to it - TODO: use command if (ive != null && ive.element instanceof Chord) { Chord chord = (Chord) ive.element; for (Pitch pitch : pitches) chord.addNote(new Note(pitch)); } } /** * Writes the given {@link VoiceElement} at the current position and, * if the moving flag is set, moves the cursor forward according to * the duration of the element. * * This method overwrites the elements overlapping the current cursor * position (but not {@link NoVoiceElement}s like key or time signatures at * the cursor position) and overlapping the current cursor position plus the * duration of the given element. * * Thus, if gaps appear before or after the written element, the corresponding * elements are cut. */ @Override public void write(VoiceElement element) throws MeasureFullException { //create the voice, if needed ensureVoiceExists(mp); //create the options VoiceElementWrite.Options options = new VoiceElementWrite.Options(); options.checkTimeSignature = true; //always obey to time signature options.fillWithHiddenRests = fillWithHiddenRests; //optionally, fill gaps with hidden rests //write the element new VoiceElementWrite(score.getVoice(mp), mp, element, options).execute(); //if a beam is open and it is a chord, add it if (openBeamWaypoints != null && element instanceof Chord) { Chord chord = (Chord) element; openBeamWaypoints.add(new BeamWaypoint(chord, false)); } //if move flag is set, move cursor forward, also over measure boundaries if (moving) { Fraction newBeat = mp.beat.add(element.getDuration()); //if this beat is behind the end of the measure, jump into the next measure Fraction measureBeats = score.getHeader().getTimeAtOrBefore(mp.measure).getType().getMeasureBeats(); if (measureBeats != null && newBeat.compareTo(measureBeats) >= 0) { //begin new measure mp = mp(mp.staff, mp.measure + 1, mp.voice, _0, 0); } else { //stay in the current measure mp = mp(mp.staff, mp.measure, mp.voice, newBeat, mp.element + 1); } } } /** * See {@link #write(VoiceElement)}. */ @Override public void write(VoiceElement element, ScoreInputOptions options) throws MeasureFullException { write(element); } /** * Writes the given {@link MeasureElement} at the current position. * Dependent on its type, it may replace elements of the same type. */ @Override public void write(MeasureElement element) { //create the measure, if needed ensureMeasureExists(mp); //write the element new MeasureElementWrite(element, score.getMeasure(mp), mp.beat).execute(); } /** * See {@link #write(MeasureElement)}. */ @Override public void write(MeasureElement element, ScoreInputOptions options) { write(element); } /** * Opens a beam. All following chords will be added to it. */ public void openBeam() { if (openBeamWaypoints != null) throw new IllegalStateException("Beam is already open"); openBeamWaypoints = new ArrayList<>(); } /** * Closes a beam and adds it to the score. */ public void closeBeam() { if (openBeamWaypoints == null) throw new IllegalStateException("No beam is open"); Beam beam = beam(openBeamWaypoints); for (BeamWaypoint wp : openBeamWaypoints) wp.getChord().setBeam(beam); openBeamWaypoints = null; } /** * Opens a slur of the given type. All following chords will be added to it. */ public void openSlur(SlurType type) { if (openSlurWaypoints != null) throw new IllegalStateException("Slur is already open"); checkArgsNotNull(type); openSlurWaypoints = new ArrayList<>(); openSlurType = type; } /** * Closes a slur and adds it to the score. * @deprecated Does not work yet. Slur waypoints are not collected in this class. */ public void closeSlur() { if (openSlurWaypoints == null) throw new IllegalStateException("No curved line is open"); Slur slur = new Slur(openSlurType, openSlurWaypoints, null); new SlurAdd(slur).execute(); openSlurWaypoints = null; } /** * Ensures, that the given given staff exists. * If not, it is created. */ private void ensureStaffExists(MP mp) { //create staves if needed if (score.getStavesCount() <= mp.staff) new StaffAdd(score, mp.staff - score.getStavesCount() + 1).execute(); } /** * Ensures, that the given measure and staff exists. * If not, it is created. */ private void ensureMeasureExists(MP mp) { //create staves if needed ensureStaffExists(mp); //create measures if needed if (score.getMeasuresCount() <= mp.measure) new MeasureAdd(score, mp.measure - score.getMeasuresCount() + 1).execute(); } /** * Ensures, that the given voice, measure and staff exists. * If not, it is created. */ private void ensureVoiceExists(MP mp) { //create measure if needed ensureMeasureExists(mp); //create voice if needed Measure measure = score.getMeasure(mp); if (measure.getVoices().size() <= mp.voice) new VoiceAdd(measure, mp.voice).execute(); } }