Java tutorial
package com.rockhoppertech.symchords.fx; /* * #%L * symchords-fx * %% * Copyright (C) 2013 - 2014 Rockhopper Technologies * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import java.awt.datatransfer.DataFlavor; import java.net.URL; import java.util.List; import java.util.ResourceBundle; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ListProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleListProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.geometry.Point2D; import javafx.scene.Node; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.RadioButton; import javafx.scene.control.Slider; import javafx.scene.control.TextField; import javafx.scene.control.ToggleGroup; import javafx.scene.effect.DropShadow; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.ClipboardContent; import javafx.scene.input.DataFormat; import javafx.scene.input.DragEvent; import javafx.scene.input.Dragboard; import javafx.scene.input.MouseEvent; import javafx.scene.input.TransferMode; import javafx.scene.layout.AnchorPane; import javafx.scene.paint.Color; import javafx.stage.Stage; import javafx.util.Callback; import org.apache.commons.lang3.ArrayUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.Lists; import com.google.common.primitives.Ints; import com.rockhoppertech.collections.ListUtils; import com.rockhoppertech.music.Duration; import com.rockhoppertech.music.Pattern; import com.rockhoppertech.music.PatternBuilder; import com.rockhoppertech.music.PatternFactory; import com.rockhoppertech.music.PitchFactory; import com.rockhoppertech.music.PitchFormat; import com.rockhoppertech.music.Timed; import com.rockhoppertech.music.fx.cmn.GrandStaff; import com.rockhoppertech.music.midi.js.MIDITrack; import com.rockhoppertech.music.midi.js.MIDITrackFactory; import com.rockhoppertech.music.midi.js.function.StartTimeFunction; import com.rockhoppertech.music.modifiers.DurationModifier; import com.rockhoppertech.music.modifiers.Modifier.Operation; import com.rockhoppertech.music.modifiers.NoteModifier; import com.rockhoppertech.music.modifiers.PitchModifier; import com.rockhoppertech.music.modifiers.StartBeatModifier; /** * @author <a href="http://genedelisa.com/">Gene De Lisa</a> * */ public class SymChordsController { private static final Logger logger = LoggerFactory.getLogger(SymChordsController.class); public void setStage(Stage stage) { this.stage = stage; } private Stage stage; @FXML private AnchorPane root; @FXML private ResourceBundle resources; @FXML private URL location; @FXML private RadioButton divideModifierCB; @FXML private RadioButton durationModifierCB; @FXML private RadioButton modModifierCB; @FXML private ToggleGroup modifierGroup; @FXML private TextField modifierValuesTextField; @FXML private RadioButton multiplyModifierCB; @FXML private ToggleGroup operationGroup; @FXML private RadioButton pitchModifierCB; @FXML private RadioButton setModifierCB; @FXML private RadioButton startBeatModifierCB; @FXML private RadioButton subtractModifierCB; @FXML private GrandStaff grandStaff; @FXML private GrandStaff patternStaff; @FXML private TextField IntervalsTextField; @FXML private ComboBox<String> baseOctaveCombo; @FXML private ComboBox<String> basePitchCombo; @FXML private Slider octavesSlider; @FXML private TextField patternTextField; @FXML private ToggleGroup relationshipGroup; @FXML private RadioButton relativeRadio; @FXML private RadioButton absoluteRadio; @FXML private Slider unitSlider; @FXML private Slider pitchSlider; @FXML private Label sliderText; @FXML private ListView<MIDITrack> trackList; @FXML private ListView<int[]> patternListView; @FXML private CheckBox patternReverseCB; @FXML private CheckBox patternSpreadCB; @FXML private CheckBox patternUpAndDownCB; @FXML private CheckBox patternUsePCCB; @FXML private RadioButton addModifierCB; private DurationModifier durationModifier = new DurationModifier(); private StartBeatModifier startBeatModifier = new StartBeatModifier(); private PitchModifier pitchModifier = new PitchModifier(); private NoteModifier noteModifier = this.durationModifier; @FXML void durationModifierAction(ActionEvent event) { noteModifier = durationModifier; modifierValuesTextField.setText(MIDITrack.getDurationsAsString(model.getMIDITrack())); } @FXML void startBeatModifierAction(ActionEvent event) { noteModifier = startBeatModifier; modifierValuesTextField.setText(MIDITrack.getStartBeatsAsString(model.getMIDITrack())); } @FXML void pitchModifierAction(ActionEvent event) { noteModifier = pitchModifier; modifierValuesTextField.setText(MIDITrack.getPitchesMIDINumbersAsString(model.getMIDITrack())); } @FXML void modifyAction(ActionEvent event) { if (subtractModifierCB.isSelected()) { noteModifier.setOperation(NoteModifier.Operation.SUBTRACT); } else if (this.setModifierCB.isSelected()) { noteModifier.setOperation(NoteModifier.Operation.SET); } else if (this.modModifierCB.isSelected()) { noteModifier.setOperation(NoteModifier.Operation.MOD); } else if (this.divideModifierCB.isSelected()) { noteModifier.setOperation(NoteModifier.Operation.DIVIDE); } else if (this.multiplyModifierCB.isSelected()) { noteModifier.setOperation(NoteModifier.Operation.MULTIPLY); } else if (this.addModifierCB.isSelected()) { noteModifier.setOperation(NoteModifier.Operation.ADD); } double[] values = getModifierValues(); if (values == null) { logger.debug("no modifier values"); return; } logger.debug(ArrayUtils.toString(values)); noteModifier.setValues(values); model.getMIDITrack().map(noteModifier); grandStaff.setTrack(model.getMIDITrack()); grandStaff.drawShapes(); } private double[] getModifierValues() { final String s = this.modifierValuesTextField.getText(); if ((s == null) || s.equals("")) { logger.debug("null values"); return null; } final String[] a = s.split("\\s+"); final double[] array = new double[a.length]; int i = 0; for (final String ss : a) { try { array[i++] = Double.parseDouble(ss); } catch (final Exception e) { logger.error(e.getLocalizedMessage(), e); } } return array; } @FXML void patternCBAction(ActionEvent event) { patternCalculate(); } @FXML void exitAction(ActionEvent event) { System.exit(0); } @FXML private CheckBox chainCB; @FXML private CheckBox mirrorCB; @FXML private CheckBox playAsChordCB; @FXML void doitAction(ActionEvent event) { model.createTrack(); MIDITrack track = model.getMIDITrack(); track.sequential(); if (this.pattern != null) { this.patternTextField.setText(ArrayUtils.toString(this.pattern)); Pattern p = new PatternBuilder(track.getPitchClasses(), this.pattern).build(); this.patternedTrack = p.createTrack(); } else { this.resetPattern(track.size()); } updatePatternList(); if (mirrorCB.isSelected()) { final MIDITrack inversion = this.model.getMIDITrack().getInversion(); inversion.remove(0); track = model.getMIDITrack(); inversion.append(track); inversion.sortByAscendingPitches(); inversion.sequential(); this.model.setMIDITrack(inversion); } if (this.chainCB.isSelected()) { // track = this.model.getMIDITrack(); track = new MIDITrack(); for (Integer midiNumber : this.model.getMIDITrack().getPitchesAsIntegers()) { MIDITrack list = MIDITrackFactory.createFromIntervals(this.model.getIntervals(), midiNumber, this.model.getUnit(), this.model.getAbsolute(), this.model.getNOctaves()); if (mirrorCB.isSelected()) { final MIDITrack inversion = list.getInversion(); inversion.remove(0); list = inversion.append(list); list.sortByAscendingPitches(); } System.err.println(list); track.append(list); } // model.setMIDITrack(this.model.getMIDITrack().append(list)); model.setMIDITrack(track); } // track = model.getMIDITrack(); // grandStaff.setTrack(track); // grandStaff.drawShapes(); logger.debug("track {}", track); logger.debug("track {}", track.getDescription()); } /* */ @FXML void playAction(ActionEvent event) { MIDITrack track = model.getMIDITrack(); if (this.playAsChordCB.isSelected()) { // StartTimeFunction function = new StartTimeFunction(); // function.setOperation(Operation.ADD); // List<Timed> newnotes = Lists.transform(track.getNotes(), // function); StartBeatModifier sbm = new StartBeatModifier(1d); sbm.setOperation(Operation.SET); track.map(sbm); } else { track.sequential(); } track.play(); //if (((Node) event.getSource()).getId().equals("")) { //} } @FXML void playSelectedAction(ActionEvent event) { grandStaff.getMIDITrack().play(); } private SymModel model; MIDITrack patternedTrack; private IntegerProperty pitchProperty = new SimpleIntegerProperty(60); @FXML void initialize() { assert IntervalsTextField != null : "fx:id=\"IntervalsTextField\" was not injected: check your FXML file 'symchords.fxml'."; assert baseOctaveCombo != null : "fx:id=\"baseOctaveCombo\" was not injected: check your FXML file 'symchords.fxml'."; assert basePitchCombo != null : "fx:id=\"basePitchCombo\" was not injected: check your FXML file 'symchords.fxml'."; assert octavesSlider != null : "fx:id=\"octavesSlider\" was not injected: check your FXML file 'symchords.fxml'."; assert patternTextField != null : "fx:id=\"patternTextField\" was not injected: check your FXML file 'symchords.fxml'."; assert relationshipGroup != null : "fx:id=\"relationshipGroup\" was not injected: check your FXML file 'symchords.fxml'."; assert relativeRadio != null : "fx:id=\"relativeRadio\" was not injected: check your FXML file 'symchords.fxml'."; assert unitSlider != null : "fx:id=\"unitSlider\" was not injected: check your FXML file 'symchords.fxml'."; grandStaff.setFontSize(24d); grandStaff.drawShapes(); patternStaff.setFontSize(24d); patternStaff.drawShapes(); baseOctaveCombo.getSelectionModel().select(5); basePitchCombo.getSelectionModel().select(0); pitchProperty.bindBidirectional(pitchSlider.valueProperty()); // sliderText.setText(Math.round(pitchSlider.getValue()) + ""); PitchFormat.getInstance().setWidth(4); sliderText.setText(PitchFormat.getInstance().format((int) pitchSlider.getValue())); pitchSlider.valueProperty().addListener(new ChangeListener<Number>() { @Override public void changed(ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) { if (newValue == null) { sliderText.setText(""); return; } basePitchCombo.setValue(PitchFormat.getPitchString(newValue.intValue())); baseOctaveCombo.setValue("" + newValue.intValue() / 12); sliderText.setText(PitchFormat.getInstance().format(newValue.intValue())); // sliderText.setText(Math.round(newValue.intValue()) + ""); } }); basePitchCombo.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> ov, String oldValue, String newValue) { int pitch = PitchFactory.getPitch(newValue + "0").getMidiNumber(); int oct = Integer.parseInt(baseOctaveCombo.getSelectionModel().getSelectedItem()); pitchProperty.set(pitch + (oct * 12)); } }); baseOctaveCombo.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> ov, String oldValue, String newValue) { int oct = Integer.parseInt(newValue); int pitch = PitchFactory.getPitch(basePitchCombo.getSelectionModel().getSelectedItem() + "0") .getMidiNumber(); pitchProperty.set(pitch + (oct * 12)); } }); // java 8 // basePitchCombo.getSelectionModel().selectedItemProperty().addListener( // (ObservableValue<? extends String> ov, String oldValue, String // newValue) -> { // int pitch = Integer.parseInt(newValue); // int oct = // Integer.parseInt(baseOctaveCombo.getSelectionModel().getSelectedItem()); // pitchProperty.set(pitch + oct); // }); // baseOctaveCombo.getSelectionModel().selectedItemProperty().addListener( // (ObservableValue<? extends String> ov, String oldValue, String // newValue) -> { // int oct = Integer.parseInt(newValue); // int pitch = // Integer.parseInt(basePitchCombo.getSelectionModel().getSelectedItem()); // pitchProperty.set(pitch + oct); // }); model = new SymModel(); model.absoluteProperty().bind(this.absoluteRadio.selectedProperty()); // model.relativeProperty().bind(this.relativeRadio.selectedProperty()); model.basePitchProperty().bind(this.pitchProperty); model.nOctavesProperty().bind(this.octavesSlider.valueProperty()); model.unitProperty().bind(this.unitSlider.valueProperty()); // set up initial value ObservableList<Integer> ol = FXCollections.observableArrayList(); ol.add(1); setIntervals(ol); // now handle input. turn the string into an array. this.IntervalsTextField.textProperty().addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> arg0, String oldValue, String newValue) { String[] a = newValue.split("\\s+"); ObservableList<Integer> ol = FXCollections.observableArrayList(); for (final String ss : a) { ol.add(Integer.parseInt(ss)); } setIntervals(ol); } }); model.intervalsProperty().bind(this.intervalsProperty); this.resetPattern(); this.patternTextField.textProperty().addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> arg0, String oldValue, String newValue) { logger.debug("new value '{}'", newValue); newValue = newValue.replaceAll("\\{", ""); newValue = newValue.replaceAll("\\}", ""); newValue = newValue.replaceAll(",", " "); logger.debug("new value '{}'", newValue); String[] a = newValue.split("\\s+"); ObservableList<Integer> ol = FXCollections.observableArrayList(); for (final String ss : a) { ol.add(Integer.parseInt(ss)); } // setIntervals(ol); } }); patternListView.setCellFactory(new Callback<ListView<int[]>, ListCell<int[]>>() { @Override public ListCell<int[]> call(ListView<int[]> list) { return new PatternListCell(); } }); updatePatternList(); patternListView.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<int[]>() { @Override public void changed(ObservableValue<? extends int[]> observable, int[] oldValue, int[] newValue) { pattern = newValue; patternCalculate(); patternTextField.setText(ArrayUtils.toString(pattern)); } }); setupDragonDrop(); trackList.setCellFactory(new Callback<ListView<MIDITrack>, ListCell<MIDITrack>>() { @Override public ListCell<MIDITrack> call(ListView<MIDITrack> list) { return new TrackListCell(); } }); trackList.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<MIDITrack>() { @Override public void changed(ObservableValue<? extends MIDITrack> observable, MIDITrack oldValue, MIDITrack newValue) { model.setMIDITrack(newValue); // SymParams params = model.getParamsForTrack(newValue); SymParams params = (SymParams) newValue.getUserData(); logger.debug("params {}", params); if (params != null) { int pitch = params.getBasePitch() % 12; String ps = PitchFormat.getPitchString(pitch); basePitchCombo.setValue(ps); String oct = "" + params.getBasePitch() / 12; baseOctaveCombo.setValue(oct); String intervals = ListUtils.asIntString(params.getIntervals()); IntervalsTextField.setText(intervals); octavesSlider.setValue(params.getnOctaves()); unitSlider.setValue(params.getUnit()); relativeRadio.setSelected(params.isRelative()); absoluteRadio.setSelected(!params.isRelative()); } } }); model.midiTrackProperty().addListener(new ChangeListener<MIDITrack>() { @Override public void changed(ObservableValue<? extends MIDITrack> arg0, MIDITrack arg1, MIDITrack newTrack) { if (durationModifierCB.isSelected()) { modifierValuesTextField.setText(MIDITrack.getDurationsAsString(newTrack)); } if (pitchModifierCB.isSelected()) { modifierValuesTextField.setText(MIDITrack.getPitchesMIDINumbersAsString(newTrack)); } if (startBeatModifierCB.isSelected()) { modifierValuesTextField.setText(MIDITrack.getStartBeatsAsString(newTrack)); } logger.debug("midi track property changed {}", newTrack); grandStaff.setTrack(newTrack); grandStaff.drawShapes(); } }); } private final ListProperty<Integer> intervalsProperty = new SimpleListProperty<>(); public void setIntervals(ObservableList<Integer> ol) { this.intervalsProperty.set(ol); } private int[] pattern; void resetPattern(int length) { pattern = new int[length]; for (int i = 0; i < length; i++) { pattern[i] = i; } } void resetPattern() { resetPattern(model.getMIDITrack().size()); this.patternTextField.setText(ArrayUtils.toString(this.pattern)); } void updatePatternList() { int limit = 7; int size = 6; List<int[]> patterns = null; if (this.model.getMIDITrack() != null) { size = model.getMIDITrack().size(); patterns = PatternFactory.getPatterns(size, size); } else { patterns = PatternFactory.getPatterns(limit, size); } ObservableList<int[]> items = FXCollections.observableArrayList(); this.patternListView.setItems(items); for (int[] a : patterns) { items.add(a); } } /** * Make the array readable as a String. * * @author <a href="http://genedelisa.com/">Gene De Lisa</a> * */ static class PatternListCell extends ListCell<int[]> { @Override public void updateItem(int[] item, boolean empty) { super.updateItem(item, empty); if (item != null) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < item.length; i++) { sb.append(item[i]).append(' '); } this.setText(sb.toString()); } } } /** * Make the track readable as a String. * * @author <a href="http://genedelisa.com/">Gene De Lisa</a> * */ static class TrackListCell extends ListCell<MIDITrack> { @Override public void updateItem(MIDITrack item, boolean empty) { super.updateItem(item, empty); if (item != null) { this.setText(item.getName() + "-" + System.currentTimeMillis()); } } } public void patternCalculate() { boolean reverse = patternReverseCB.isSelected(); boolean upAndDown = patternUpAndDownCB.isSelected(); boolean spread = patternSpreadCB.isSelected(); boolean usePitchClasses = patternUsePCCB.isSelected(); int nOctaves = 1; double duration = Duration.Q; double restBetweenPatterns = Duration.SIXTEENTH_NOTE; this.pattern = patternListView.getSelectionModel().getSelectedItem(); if (this.pattern != null && model.getMIDITrack() != null) { Pattern p = null; if (usePitchClasses) { int[] degrees = Ints.toArray(model.getMIDITrack().getPitchClasses()); p = PatternBuilder.create().degrees(degrees).pattern(this.pattern).duration(duration) .numOctaves(nOctaves).reverse(reverse).upAndDown(upAndDown).build(); spread = true; } else { List<Integer> pitches = model.getMIDITrack().getPitchesAsIntegers(); p = PatternBuilder.create().pattern(pattern).degrees(Ints.toArray(pitches)).duration(duration) .numOctaves(nOctaves).startPitch(pitches.get(0)).reverse(reverse).upAndDown(upAndDown) .restBetweenPatterns(restBetweenPatterns).build(); } this.patternedTrack = p.createTrack(0d, spread); this.patternStaff.setTrack(this.patternedTrack); patternStaff.drawShapes(); updatePatternList(); logger.debug("pattern {}", p); logger.debug("patterned track {}", this.patternedTrack); } else { this.resetPattern(model.getMIDITrack().size()); } } // dragon drop private ImageView dragImageView; protected void setupDragonDrop() { Image image = new Image(getClass().getResourceAsStream("/images/rocky-32-trans.png")); dragImageView = new ImageView(image); dragImageView.setFitHeight(32); dragImageView.setFitWidth(32); grandStaff.setOnDragDetected(new EventHandler<MouseEvent>() { @Override public void handle(MouseEvent me) { if (!root.getChildren().contains(dragImageView)) { root.getChildren().add(dragImageView); } // dragImageView.setOpacity(0.5); dragImageView.toFront(); dragImageView.setMouseTransparent(true); dragImageView.setVisible(true); dragImageView.relocate((int) (me.getSceneX() - dragImageView.getBoundsInLocal().getWidth() / 2), (int) (me.getSceneY() - dragImageView.getBoundsInLocal().getHeight() / 2)); Dragboard db = grandStaff.startDragAndDrop(TransferMode.ANY); // TODO remove the custom image nonsense in javafx 8 // javafx 8 // db.setDragView(dragImageView); ClipboardContent content = new ClipboardContent(); // MIDITrack track = grandStaff.getMIDITrack(); MIDITrack track = model.getMIDITrack(); content.put(midiTrackDataFormat, track); db.setContent(content); me.consume(); } }); grandStaff.setOnDragDone(new EventHandler<DragEvent>() { public void handle(DragEvent e) { dragImageView.setVisible(false); e.consume(); } }); // Parent root = grandStaff.getScene().getRoot(); // stage.getScene().getRoot(); if (root != null) { root.setOnDragOver(new EventHandler<DragEvent>() { public void handle(DragEvent e) { Point2D localPoint = grandStaff.getScene().getRoot() .sceneToLocal(new Point2D(e.getSceneX(), e.getSceneY())); dragImageView.relocate( (int) (localPoint.getX() - dragImageView.getBoundsInLocal().getWidth() / 2), (int) (localPoint.getY() - dragImageView.getBoundsInLocal().getHeight() / 2)); e.consume(); } }); } trackList.setOnDragOver(new EventHandler<DragEvent>() { public void handle(DragEvent event) { /* * data is dragged over the target; accept it only if it is not * dragged from the same node and if it has MIDITrack data */ if (event.getGestureSource() != trackList && event.getDragboard().hasContent(midiTrackDataFormat)) { logger.debug("drag over"); /* allow for both copying and moving, whatever user chooses */ event.acceptTransferModes(TransferMode.COPY_OR_MOVE); } // Don't consume the event. Let the layers below process the // DragOver event as well so that the // translucent container image will follow the cursor. // event.consume(); } }); trackList.setOnDragEntered(new EventHandler<DragEvent>() { public void handle(DragEvent event) { /* the drag-and-drop gesture entered the target */ /* show to the user that it is an actual gesture target */ logger.debug("drag entered"); if (event.getGestureSource() != trackList && event.getDragboard().hasContent(midiTrackDataFormat)) { DropShadow dropShadow = new DropShadow(); dropShadow.setRadius(5.0); dropShadow.setOffsetX(3.0); dropShadow.setOffsetY(3.0); dropShadow.setColor(Color.color(0.4, 0.5, 0.5)); trackList.setEffect(dropShadow); } event.consume(); } }); trackList.setOnDragExited(new EventHandler<DragEvent>() { public void handle(DragEvent event) { /* mouse moved away, remove the graphical cues */ trackList.setEffect(null); event.consume(); } }); trackList.setOnDragDropped(new EventHandler<DragEvent>() { public void handle(DragEvent event) { Dragboard db = event.getDragboard(); boolean success = false; if (db.hasContent(midiTrackDataFormat)) { MIDITrack track = (MIDITrack) db.getContent(midiTrackDataFormat); trackList.getItems().add(track); success = true; } /* * let the source know whether the data was successfully * transferred and used */ event.setDropCompleted(success); event.consume(); } }); logger.debug("jvm mime {}", DataFlavor.javaJVMLocalObjectMimeType); } /** * For Dragon drop. * <p> * The string HAS to be in this format, otherwise on OSX, you'll get * "Cannot set data for an invalid UTI" */ private static final DataFormat midiTrackDataFormat = new DataFormat( "com.rockhoppertech.music.midi.js.MIDITrack"); }