/* * Copyright (C) 2013-2017 たんらる */ package fourthline.mabiicco.ui.editor; import java.awt.Cursor; import java.awt.Frame; import java.awt.IllegalComponentStateException; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Arrays; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.SwingUtilities; import javax.swing.event.MouseInputListener; import fourthline.mabiicco.ActionDispatcher; import fourthline.mabiicco.AppResource; import fourthline.mabiicco.IEditState; import fourthline.mabiicco.IEditStateObserver; import fourthline.mabiicco.midi.IPlayNote; import fourthline.mabiicco.midi.MabiDLS; import fourthline.mabiicco.ui.IMMLManager; import fourthline.mabiicco.ui.PianoRollView; import fourthline.mmlTools.MMLEventList; import fourthline.mmlTools.MMLNoteEvent; public final class MMLEditor implements MouseInputListener, IEditState, IEditContext, IEditAlign { private EditMode editMode = EditMode.SELECT; // 編集選択中のノート private final ArrayList<MMLNoteEvent> selectedNote = new ArrayList<>(); // 複数ノート移動時のdetachリスト private final ArrayList<MMLNoteEvent> detachedNote = new ArrayList<>(); // Cut, Copy時に保持するリスト. private MMLEventList clipEventList; // 編集align (tick base) private int editAlign = 48; private IEditStateObserver editObserver; private final PianoRollView pianoRollView; private final IPlayNote notePlayer; private final IMMLManager mmlManager; private final JPopupMenu popupMenu = new JPopupMenu(); private final VelocityChangeMenu velocityChangeMenu; private final Frame parentFrame; public MMLEditor(Frame parentFrame, IPlayNote notePlayer, PianoRollView pianoRoll, IMMLManager mmlManager) { this.notePlayer = notePlayer; this.pianoRollView = pianoRoll; this.mmlManager = mmlManager; this.parentFrame = parentFrame; pianoRoll.setSelectNote(selectedNote); velocityChangeMenu = new VelocityChangeMenu(popupMenu, () -> popupTargetNote.getVelocity(), t -> { mmlManager.getActiveMMLPart().setVelocityCommand(popupTargetNote, t); mmlManager.updateActivePart(true); }); newPopupMenu(AppResource.appText("part_change"), ActionDispatcher.PART_CHANGE); newPopupMenu(AppResource.appText("edit.select_previous_all"), ActionDispatcher.SELECT_PREVIOUS_ALL); newPopupMenu(AppResource.appText("edit.select_after_all"), ActionDispatcher.SELECT_AFTER_ALL); newPopupMenu(AppResource.appText("menu.delete"), ActionDispatcher.DELETE, AppResource.appText("menu.delete.icon")); newPopupMenu(AppResource.appText("note.properties"), ActionDispatcher.NOTE_PROPERTY); } public void setEditAlign(int alignTick) { editAlign = alignTick; } @Override public int getEditAlign() { return editAlign; } /** * Editor reset */ public void reset() { selectNote(null); } private void selectNote(MMLNoteEvent noteEvent) { selectNote(noteEvent, false); } private void selectNote(MMLNoteEvent noteEvent, boolean multiSelect) { if (noteEvent == null) { selectedNote.clear(); } if ( (noteEvent != null) && (!selectedNote.contains(noteEvent))) { if (!multiSelect) { selectedNote.clear(); } selectedNote.add(noteEvent); } } private void selectMultipleNote(MMLNoteEvent noteEvent1, MMLNoteEvent noteEvent2) { selectMultipleNote(noteEvent1, noteEvent2, true); } /** * @param noteEvent1 * @param noteEvent2 * @param lookNote falseの場合は、tickOffset間にあるすべてのノートが選択される. trueの場合はnote情報もみて判定する. */ private void selectMultipleNote(MMLNoteEvent noteEvent1, MMLNoteEvent noteEvent2, boolean lookNote) { int note[] = { noteEvent1.getNote(), noteEvent2.getNote() }; int tickOffset[] = { noteEvent1.getTickOffset(), noteEvent2.getTickOffset() }; Arrays.sort(note); Arrays.sort(tickOffset); if (!lookNote) { note[0] = 0; note[1] = 96; } selectedNote.clear(); MMLEventList editEventList = mmlManager.getActiveMMLPart(); if (editEventList == null) { return; } for (MMLNoteEvent noteEvent : editEventList.getMMLNoteEventList()) { if ( (noteEvent.getNote() >= note[0]) && (noteEvent.getNote() <= note[1]) && (noteEvent.getEndTick() > tickOffset[0]) && (noteEvent.getTickOffset() <= tickOffset[1]) ) { selectedNote.add(noteEvent); } } } /** * @param point nullのときはクリアする. */ @Override public void selectNoteByPoint(Point point, int selectModifiers) { if (point == null) { selectNote(null); } else { int note = pianoRollView.convertY2Note(point.y); long tickOffset = pianoRollView.convertXtoTick(point.x); MMLEventList editEventList = mmlManager.getActiveMMLPart(); if (editEventList == null) { return; } MMLNoteEvent noteEvent = editEventList.searchOnTickOffset(tickOffset); if (noteEvent.getNote() != note) { return; } if ( (selectedNote.size() == 1) && ((selectModifiers & ActionEvent.SHIFT_MASK) != 0) ) { selectMultipleNote(selectedNote.get(0), noteEvent, false); } else { selectNote(noteEvent, ((selectModifiers & ActionEvent.CTRL_MASK) != 0)); } } } /** * 指定されたPointに新しいノートを作成する. * 作成されたNoteは、選択状態になる. */ @Override public void newMMLNoteAndSelected(Point p) { int note = pianoRollView.convertY2Note(p.y); long tickOffset = pianoRollView.convertXtoTick(p.x); long alignedTickOffset = tickOffset - (tickOffset % editAlign); MMLEventList editEventList = mmlManager.getActiveMMLPart(); if (editEventList == null) { return; } MMLNoteEvent prevNote = editEventList.searchPrevNoteOnTickOffset(tickOffset); MMLNoteEvent noteEvent = new MMLNoteEvent(note, editAlign, (int)alignedTickOffset); if (prevNote != null) { noteEvent.setVelocity(prevNote.getVelocity()); } selectNote(noteEvent); notePlayer.playNote( note, noteEvent.getVelocity() ); } /** * 選択状態のノート、ノート長を更新する(ノート挿入時) */ @Override public void updateSelectedNoteAndTick(Point p, boolean updateNote) { if (selectedNote.size() <= 0) { return; } pianoRollView.onViewScrollPoint(p); MMLNoteEvent noteEvent = selectedNote.get(0); int note = pianoRollView.convertY2Note(p.y); long tickOffset = pianoRollView.convertXtoTick(p.x); long alignedTickOffset = tickOffset - (tickOffset % editAlign); long newTick = (alignedTickOffset - noteEvent.getTickOffset()) + editAlign; if (newTick < 0) { newTick = 0; } if (updateNote) { noteEvent.setNote(note); } noteEvent.setTick((int)newTick); notePlayer.playNote(noteEvent.getNote(), noteEvent.getVelocity()); } @Override public void detachSelectedMMLNote() { detachedNote.clear(); for (MMLNoteEvent noteEvent : selectedNote) { detachedNote.add(noteEvent.clone()); } } /** * 選択状態のノートを移動する */ @Override public void moveSelectedMMLNote(Point start, Point p, boolean shiftOption) { pianoRollView.onViewScrollPoint(p); long targetTick = pianoRollView.convertXtoTick(start.x); int noteDelta = pianoRollView.convertY2Note(p.y) - pianoRollView.convertY2Note(start.y); long tickOffsetDelta = pianoRollView.convertXtoTick(p.x) - targetTick; long alignedTickOffsetDelta = tickOffsetDelta - (tickOffsetDelta % editAlign); if (shiftOption) { alignedTickOffsetDelta = 0; } int velocity = detachedNote.get(0).getVelocity(); for (int i = 0; i < selectedNote.size(); i++) { MMLNoteEvent note1 = detachedNote.get(i); MMLNoteEvent note2 = selectedNote.get(i); note2.setNote(note1.getNote() + noteDelta); note2.setTickOffset(note1.getTickOffset() + (int)alignedTickOffsetDelta); if ( (note1.getTickOffset() <= targetTick) && (note1.getEndTick() > targetTick) ) { velocity = note2.getVelocity(); } } notePlayer.playNote( pianoRollView.convertY2Note(p.y), velocity ); } @Override public void cancelMove() { int i = 0; for (MMLNoteEvent noteEvent : selectedNote) { MMLNoteEvent revertNote = detachedNote.get(i++); noteEvent.setNote(revertNote.getNote()); noteEvent.setTickOffset(revertNote.getTickOffset()); } } /** * 編集選択中のノートをイベントリストに反映する. * @param select trueの場合は選択状態を維持する. */ @Override public void applyEditNote(boolean select) { MMLEventList editEventList = mmlManager.getActiveMMLPart(); if (editEventList == null) { return; } for (MMLNoteEvent noteEvent : selectedNote) { editEventList.deleteMMLEvent(noteEvent); editEventList.addMMLNoteEvent(noteEvent); } if (!select) { selectNote(null); } notePlayer.offNote(); mmlManager.updateActivePart(true); } @Override public void setCursor(Cursor cursor) { pianoRollView.setCursor(cursor); } @Override public void areaSelectingAction(Point startPoint, Point point) { int x1 = startPoint.x; int x2 = point.x; if (x1 > x2) { x2 = startPoint.x; x1 = point.x; } int y1 = startPoint.y; int y2 = point.y; if (y1 > y2) { y2 = startPoint.y; y1 = point.y; } Rectangle rect = new Rectangle(x1, y1, (x2-x1), (y2-y1)); pianoRollView.setSelectingArea(rect); int note1 = pianoRollView.convertY2Note( y1 ); int tickOffset1 = (int)pianoRollView.convertXtoTick( x1 ); int note2 = pianoRollView.convertY2Note( y2 ); int tickOffset2 = (int)pianoRollView.convertXtoTick( x2 ); selectMultipleNote( new MMLNoteEvent(note1, 0, tickOffset1, 0), new MMLNoteEvent(note2, 0, tickOffset2, 0) ); } @Override public void applyAreaSelect() { pianoRollView.setSelectingArea(null); } /** * Editスタートポイントがノート上であるかどうかを判定する. * @param point * @return ノート上の場合はtrue. */ @Override public boolean onExistNote(Point point) { int note = pianoRollView.convertY2Note( point.y ); int tickOffset = (int)pianoRollView.convertXtoTick( point.x ); MMLEventList editEventList = mmlManager.getActiveMMLPart(); if (editEventList == null) { return false; } MMLNoteEvent noteEvent = editEventList.searchOnTickOffset(tickOffset); if ( (noteEvent != null) && (note == noteEvent.getNote()) ) { return true; } return false; } /** * pointの位置で他トラックにノートが配置されていれば、アクティブパートを変更します. * @param point * @return アクティブパートを変更した場合はtrue. */ @Override public boolean selectTrackOnExistNote(Point point) { if (onExistNote(point)) { return false; } int note = pianoRollView.convertY2Note( point.y ); int tickOffset = (int)pianoRollView.convertXtoTick( point.x ); return mmlManager.selectTrackOnExistNote(note, tickOffset); } /** * 音価編集位置にあるかどうかを判定する. * @param point * @return */ @Override public boolean isEditLengthPosition(Point point) { int note = pianoRollView.convertY2Note( point.y ); int tickOffset = (int)pianoRollView.convertXtoTick( point.x ); MMLEventList editEventList = mmlManager.getActiveMMLPart(); if (editEventList == null) { return false; } MMLNoteEvent noteEvent = editEventList.searchOnTickOffset( tickOffset ); if ( (noteEvent != null) && (noteEvent.getNote() == note) ) { if (noteEvent.getEndTick() <= tickOffset + (noteEvent.getTick() / 5) ) { return true; } } return false; } /** * Edit状態を変更する. * @param nextMode */ @Override public EditMode changeState(EditMode nextMode) { if (editMode != nextMode) { editMode.exit(this); editMode = nextMode; editMode.enter(this); } return nextMode; } @Override public void mouseClicked(MouseEvent e) { if (SwingUtilities.isLeftMouseButton(e)) { if (e.getClickCount() == 2) { noteProperty(); } } } @Override public void mouseEntered(MouseEvent e) {} @Override public void mouseExited(MouseEvent e) {} @Override public void mousePressed(MouseEvent e) { editMode.pressEvent(this, e); pianoRollView.repaint(); } @Override public void mouseReleased(MouseEvent e) { editMode.releaseEvent(this, e); pianoRollView.repaint(); editObserver.notifyUpdateEditState(); } @Override public void mouseDragged(MouseEvent e) { editMode.executeEvent(this, e); pianoRollView.repaint(); } @Override public void mouseMoved(MouseEvent e) { editMode.executeEvent(this, e); } @Override public boolean hasSelectedNote() { return !(selectedNote.isEmpty()); } @Override public void setEditStateObserver(IEditStateObserver observer) { this.editObserver = observer; } @Override public boolean canPaste() { if (clipEventList == null) { return false; } if (clipEventList.getMMLNoteEventList().size() == 0) { return false; } return true; } @Override public void paste(long startTick) { if (!canPaste()) { return; } MMLEventList editEventList = mmlManager.getActiveMMLPart(); if (editEventList == null) { return; } selectNote(null); int delta = (int)( startTick - clipEventList.getMMLNoteEventList().get(0).getTickOffset() ); for (MMLNoteEvent noteEvent : clipEventList.getMMLNoteEventList()) { MMLNoteEvent addNote = noteEvent.clone(); addNote.setTickOffset(noteEvent.getTickOffset() + delta); editEventList.addMMLNoteEvent(addNote); selectNote(addNote, true); } editObserver.notifyUpdateEditState(); mmlManager.updateActivePart(true); } @Override public void selectedCut() { MMLEventList editEventList = mmlManager.getActiveMMLPart(); if (editEventList == null) { return; } clipEventList = new MMLEventList(""); for (MMLNoteEvent noteEvent : selectedNote) { clipEventList.addMMLNoteEvent(noteEvent); editEventList.deleteMMLEvent(noteEvent); } selectNote(null); editObserver.notifyUpdateEditState(); mmlManager.updateActivePart(true); } @Override public void selectedCopy() { clipEventList = new MMLEventList(""); for (MMLNoteEvent noteEvent : selectedNote) { clipEventList.addMMLNoteEvent(noteEvent); } editObserver.notifyUpdateEditState(); } @Override public void selectedDelete() { MMLEventList editEventList = mmlManager.getActiveMMLPart(); if (editEventList == null) { return; } for (MMLNoteEvent noteEvent : selectedNote) { editEventList.deleteMMLEvent(noteEvent); } selectNote(null); editObserver.notifyUpdateEditState(); mmlManager.updateActivePart(true); } @Override public void noteProperty() { if (selectedNote.isEmpty()) { return; } MMLEventList editEventList = mmlManager.getActiveMMLPart(); if (editEventList != null) { new MMLNotePropertyPanel(selectedNote.toArray(new MMLNoteEvent[selectedNote.size()]), editEventList).showDialog(parentFrame); mmlManager.updateActivePart(true); } } @Override public void selectAll() { MMLEventList editEventList = mmlManager.getActiveMMLPart(); if (editEventList != null) { selectedNote.clear(); selectedNote.addAll(editEventList.getMMLNoteEventList()); } } @Override public void selectPreviousAll() { MMLEventList editEventList = mmlManager.getActiveMMLPart(); if (editEventList != null) { for (MMLNoteEvent note : editEventList.getMMLNoteEventList()) { if (note.getTickOffset() < popupTargetNote.getTickOffset()) { selectedNote.add(note); } else { break; } } } } @Override public void selectAfterAll() { MMLEventList editEventList = mmlManager.getActiveMMLPart(); if (editEventList != null) { for (MMLNoteEvent note : editEventList.getMMLNoteEventList()) { if (note.getTickOffset() > popupTargetNote.getTickOffset()) { selectedNote.add(note); } } } } public void changePart(MMLEventList from, MMLEventList to, boolean useSelectedNoteList, ChangePartAction action) { int startTick = 0; int endTick; if (useSelectedNoteList && (selectedNote.size() > 0) ) { MMLNoteEvent startNote = selectedNote.get(0); MMLNoteEvent endNote = selectedNote.get(selectedNote.size()-1); startTick = from.getAlignmentStartTick(to, startNote.getTickOffset()); endTick = from.getAlignmentEndTick(to, endNote.getEndTick()); } else { endTick = (int) from.getTickLength(); int toEndTick = (int) to.getTickLength(); if (endTick < toEndTick) { endTick = toEndTick; } } switch (action) { case SWAP: from.swap(to, startTick, endTick); break; case MOVE: from.move(to, startTick, endTick); break; case COPY: from.copy(to, startTick, endTick); break; default: } } public enum ChangePartAction { SWAP, MOVE, COPY } private JMenuItem newPopupMenu(String name, String command) { JMenuItem menu = new JMenuItem(name); menu.addActionListener(ActionDispatcher.getInstance()); menu.setActionCommand(command); popupMenu.add(menu); return menu; } private JMenuItem newPopupMenu(String name, String command, String iconName) { JMenuItem menu = newPopupMenu(name, command); try { menu.setIcon(AppResource.getImageIcon(iconName)); } catch (NullPointerException e) {} return menu; } private MMLNoteEvent popupTargetNote; @Override public void showPopupMenu(Point point) { if (MabiDLS.getInstance().getSequencer().isRecording()) { return; } int tickOffset = (int)pianoRollView.convertXtoTick( point.x ); MMLEventList editEventList = mmlManager.getActiveMMLPart(); if (editEventList == null) { return; } popupTargetNote = editEventList.searchOnTickOffset(tickOffset); velocityChangeMenu.setValue(popupTargetNote.getVelocity()); if (hasSelectedNote()) { try { popupMenu.show(pianoRollView, point.x, point.y); } catch (IllegalComponentStateException e) {} } } }