/*
* Copyright (C) 2013-2017 たんらる
*/
package fourthline.mabiicco.ui;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Stroke;
import java.util.List;
import javax.swing.JPanel;
import javax.swing.JViewport;
import javax.swing.event.MouseInputListener;
import fourthline.mabiicco.AppResource;
import fourthline.mabiicco.MabiIccoProperties;
import fourthline.mabiicco.midi.InstClass;
import fourthline.mabiicco.midi.MabiDLS;
import fourthline.mabiicco.ui.color.ColorManager;
import fourthline.mmlTools.MMLEventList;
import fourthline.mmlTools.MMLNoteEvent;
import fourthline.mmlTools.MMLScore;
import fourthline.mmlTools.MMLTrack;
import fourthline.mmlTools.core.MMLTicks;
import fourthline.mmlTools.core.UndefinedTickException;
/**
* ピアノロール表示を行うためのビューです.
*/
public final class PianoRollView extends JPanel {
private static final long serialVersionUID = -7229093886476553295L;
public static final int OCTNUM = 9;
/**
* ノートの表示高さ
*/
public static int NOTE_HEIGHT_TABLE[] = { 6, 8, 10, 12, 14 };
private int noteHeight = NOTE_HEIGHT_TABLE[3];
public int getNoteHeight() {
return noteHeight;
}
public void setNoteHeightIndex(int index) {
if ( (index >= 0) && (index < NOTE_HEIGHT_TABLE.length) ) {
noteHeight = NOTE_HEIGHT_TABLE[index];
}
}
public int getTotalHeight() {
return (12*OCTNUM*noteHeight)+noteHeight;
}
private double wideScale = 6; // ピアノロールの拡大/縮小率 (1~6)
private JViewport viewport;
private IMMLManager mmlManager;
private long sequencePosition = 0;
private long runningSequencePosition = 0;
// 描画位置判定用 (tick base)
private long startViewTick;
private long endViewTick;
// 選択中のノートイベント
private List<MMLNoteEvent> selectNoteList;
// 選択用の枠
private Rectangle selectingRect;
// draw pitch range
private int lowerNote = 0;
private int upperNote = 14;
private static final Color wKeyColor = new Color(0.9f, 0.9f, 0.9f); // 白鍵盤用
private static final Color bKeyColor = new Color(0.8f, 0.8f, 0.8f); // 黒鍵盤用
private static final Color borderColor = new Color(0.6f, 0.6f, 0.6f); // 境界線用
private static final Color pitchRangeBorderColor = Color.RED; // pitch range
private static final Color keyColors[] = new Color[] {
wKeyColor,
bKeyColor,
wKeyColor,
bKeyColor,
wKeyColor,
bKeyColor,
wKeyColor,
wKeyColor,
bKeyColor,
wKeyColor,
bKeyColor,
wKeyColor
};
private static final Color noSoundColor = new Color(0.9f, 0.8f, 0.8f);
private static final Color barBorder = new Color(0.5f, 0.5f, 0.5f);
private static final Color darkBarBorder = new Color(0.3f, 0.2f, 0.3f);
private static final Color shadowColor = Color.GRAY;
private static final int DRAW_START_MARGIN = 192;
public enum PaintMode {
ALL_TRACK("paintMode.all_track"),
ACTIVE_TRACK("paintMode.active_track"),
ACTIVE_PART("paintMode.active_part");
final private String resourceName;
private PaintMode(String name) {
resourceName = name;
}
public String toString() {
return AppResource.appText(resourceName);
}
}
private PaintMode paintMode = PaintMode.ALL_TRACK;
public PaintMode getPaintMode() {
return paintMode;
}
public void setPaintMode(PaintMode mode) {
paintMode = mode;
}
/**
* Create the panel.
*/
public PianoRollView() {
super();
setPreferredSize(new Dimension(0, getTotalHeight()));
MabiIccoProperties properties = MabiIccoProperties.getInstance();
setNoteHeightIndex( properties.getPianoRollViewHeightScaleProperty() );
setSequenceTick(0);
}
/**
* ピアノロール上で編集を行うためのマウス入力のイベントリスナーを登録します.
* @param listener 編集時処理を行うMMLEditor.
*/
public void addMouseInputListener(MouseInputListener listener) {
this.addMouseListener(listener);
this.addMouseMotionListener(listener);
}
public void setViewportAndParent(JViewport viewport, IMMLManager mmlManager) {
this.viewport = viewport;
this.mmlManager = mmlManager;
}
public void setWidth(int width) {
super.setPreferredSize(new Dimension(width, getTotalHeight()));
revalidate();
}
public void setSelectNote(List<MMLNoteEvent> list) {
selectNoteList = list;
}
public void setSelectingArea(Rectangle rect) {
selectingRect = rect;
}
/**
* 現在のトラックの内容に合わせた幅に設定します.
*/
private void updateViewWidthTrackLength() {
long tickLength = 0;
MMLScore mmlScore = mmlManager.getMMLScore();
for (MMLTrack track : mmlScore.getTrackList()) {
long length = track.getMaxTickLength();
if (tickLength < length) {
tickLength = length;
}
}
try {
// 最後に4小節分のマージンを作成します.
int t1 = MMLTicks.getTick("1");
tickLength += t1*4;
tickLength -= tickLength % t1;
} catch (UndefinedTickException e) {
e.printStackTrace();
}
setWidth( convertTicktoX(tickLength) );
}
/**
* ピアノロールの横方向の拡大/縮小スケールを設定します.
* @param scale
*/
public void setWideScale(double scale) {
if ( (scale > 6.0) || (scale < 0.1) ) {
return;
}
// 拡大/縮小したときの表示幅を調整します.
this.wideScale = scale;
updateViewWidthTrackLength();
}
/**
* pointが表示領域になければ、Viewportをスクロールする.
* pointの位置は表示領域内に補正される.
* @param point
*/
public void onViewScrollPoint(Point point) {
int y = point.y;
int y1 = viewport.getViewPosition().y;
int y2 = y1 + viewport.getHeight() - noteHeight;
int x = viewport.getViewPosition().x;
if (x + viewport.getWidth() < point.x) {
x++;
}
if (y < y1) {
// up-scroll
y1 -= noteHeight;
if (y1 < 0) {
y1 = 0;
}
y = y1;
} else if (y > y2) {
// down-scroll
y1 += noteHeight;
if (y1 > getHeight() - viewport.getHeight()) {
y1 = getHeight() - viewport.getHeight();
y = y2;
} else {
y = y2 + noteHeight;
}
}
viewport.setViewPosition(new Point(x, y1));
point.y = y;
}
public double getWideScale() {
return this.wideScale;
}
public long convertXtoTick(int x) {
return (long)(x * wideScale);
}
public int convertTicktoX(long tick) {
return (int)(tick / wideScale);
}
/**
* Panel上のy座標をnote番号に変換します.
* @param y
* @return
*/
public final int convertY2Note(int y) {
int note = -1;
if (y >= 0) {
note = (OCTNUM*12-(y/noteHeight)) -1;
}
return note;
}
/**
* note番号をPanel上のy座標に変換します.
* @param note
* @return
*/
public final int convertNote2Y(int note) {
int y = OCTNUM*12 - note - 1;
y *= noteHeight;
return y;
}
public long getSequencePosition() {
return sequencePosition;
}
public int getSequenceX() {
return convertTicktoX( sequencePosition );
}
public void updateRunningSequencePosition() {
runningSequencePosition = MabiDLS.getInstance().getSequencer().getTickPosition();
}
public long getSequencePlayPosition() {
long position = sequencePosition;
if (MabiDLS.getInstance().getSequencer().isRunning()) {
return runningSequencePosition;
}
return position;
}
public void setSequenceTick(long tick) {
if (!MabiDLS.getInstance().getSequencer().isRunning()) {
if (tick < 0) {
tick = 0;
}
sequencePosition = tick;
}
}
/**
* 現在の描画位置 tick値を更新します.
*/
private void updateViewTick() {
double x = viewport.getViewPosition().getX();
double width = viewport.getExtentSize().getWidth();
startViewTick = convertXtoTick((int)x);
endViewTick = convertXtoTick((int)(x + width));
}
private boolean showAllVelocity = false;
/**
* 1オクターブ 12 x 6
* 9オクターブ分つくると、648
*/
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
showAllVelocity = MabiIccoProperties.getInstance().showAllVelocity.get();
updateViewTick();
// FIXME: しぼったほうがいいかも?
updateViewWidthTrackLength();
Graphics2D g2 = (Graphics2D)g.create();
for (int i = 0; i <= OCTNUM; i++) {
paintOctPianoLine(g2, i, (char)('0'+OCTNUM-i-1));
}
paintMeasure(g2);
paintPitchRangeBorder(g2);
paintOtherTrack(g2);
paintActiveTrack(g2);
paintSelectedNote(g2);
paintSelectingArea(g2);
paintSequenceLine(g2, getTotalHeight());
g2.dispose();
}
private void paintOctPianoLine(Graphics2D g, int pos, char posText) {
int startY = 12 * noteHeight * pos;
int octave = OCTNUM - pos - 1;
// グリッド
int y = startY;
int width = getWidth();
for (int i = 0; i < 12; i++) {
int line = octave*12 + (11-i);
Color fillColor = keyColors[i];
if ( (line < lowerNote) || (line > upperNote) ) {
if (MabiIccoProperties.getInstance().viewRange.get()) {
fillColor = noSoundColor;
}
}
g.setColor(fillColor);
g.fillRect(0, i*noteHeight+y, width, noteHeight);
if (i == 0) {
g.setColor(darkBarBorder);
} else {
g.setColor(borderColor);
}
g.drawLine(0, i*noteHeight+y, width, i*noteHeight+y);
}
g.setColor(darkBarBorder);
g.drawLine(0, 12*noteHeight+y, width, 12*noteHeight+y);
}
private void paintPitchRangeBorder(Graphics2D g) {
if (MabiIccoProperties.getInstance().viewRange.get()) {
int width = getWidth();
int y1 = convertNote2Y(lowerNote-1);
int y2 = convertNote2Y(upperNote);
g.setColor(pitchRangeBorderColor);
g.drawLine(0, y1, width, y1);
g.drawLine(0, y2, width, y2);
}
}
void paintSequenceLine(Graphics2D g, int height) {
long position = getSequencePlayPosition();
Color color = Color.RED;
int x = convertTicktoX(position);
g.setColor(color);
g.drawLine(x, 0, x, height);
}
/**
* 現在のスコアを基準とした1小節の幅を取得する.
* @return
*/
public int getMeasureWidth() {
try {
int sect = MMLTicks.getTick(mmlManager.getMMLScore().getBaseOnly());
sect = convertTicktoX(sect);
int borderCount = mmlManager.getMMLScore().getTimeCountOnly();
return (sect * borderCount);
} catch (UndefinedTickException e) {
throw new AssertionError();
}
}
/**
* 補助線の描画.
*/
private static final float dash[] = { 2.0f, 4.0f };
private static final BasicStroke dashStroke = new BasicStroke(1.0f,
BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER,
10.0f,
dash,
0.0f);
private void paintHalfMeasure(Graphics2D g, int offset, int w) {
int y = getTotalHeight();
Stroke oldStroke = g.getStroke();
g.setStroke(dashStroke);
g.setColor(barBorder);
int step = w;
while (step >= 32) {
step /= 2;
}
for (int x = offset + step; x < (offset + w); x+=step) {
g.drawLine(x, 0, x, y);
}
g.setStroke(oldStroke);
}
/**
* メジャーを表示します。
*/
private void paintMeasure(Graphics2D g) {
int width = (int)convertXtoTick(getWidth());
try {
int sect = MMLTicks.getTick(mmlManager.getMMLScore().getBaseOnly());
int borderCount = mmlManager.getMMLScore().getTimeCountOnly();
int y = getTotalHeight();
for (int i = 0; i*sect < width; i++) {
if (i*sect < startViewTick-sect) {
continue;
}
if (i*sect > endViewTick) {
break;
}
if (i%borderCount == 0) {
g.setColor(darkBarBorder);
} else {
g.setColor(barBorder);
}
int x = convertTicktoX(i*sect);
g.drawLine(x, 0, x, y);
paintHalfMeasure(g, x, convertTicktoX(sect));
}
} catch (UndefinedTickException e) {
e.printStackTrace();
}
}
private void drawRect(Graphics2D g, Color rectColor, Color fillColor, int x, int y, int width, int height) {
g.setColor(fillColor);
if (width != 0) {
g.fillRect(x+1, y+1, width, height-1);
} else {
g.drawLine(x+1, y+1, x+1, y+height-1);
}
g.setColor(rectColor);
g.drawLine(x+1, y+1, x+1, y+height-1);
g.drawLine(x+width+1, y+height-1, x+width+1, y+1);
g.drawLine(x+2, y, x+width, y);
g.drawLine(x+width, y+height, x+2, y+height);
}
private void drawNote(Graphics2D g, MMLNoteEvent noteEvent, Color rectColor, Color fillColor, boolean drawOption, MMLNoteEvent prevNote) {
int note = noteEvent.getNote();
int tick = noteEvent.getTick();
int offset = noteEvent.getTickOffset();
int x = convertTicktoX(offset);
int y = convertNote2Y(note) +1;
int width = convertTicktoX(tick) -1;
int height = noteHeight-2;
if (width > 1) width--;
if (drawOption) {
// shadow
drawRect(g, shadowColor, shadowColor, x+2, y+2, width, height);
}
drawRect(g, rectColor, fillColor, x, y, width, height);
if (drawOption) {
// velocityの描画.
int velocity = noteEvent.getVelocity();
if ( showAllVelocity || (prevNote == null) || (prevNote.getVelocity() != velocity) ) {
String s = "V" + velocity;
g.setColor(Color.DARK_GRAY);
g.drawString(s, x, y);
}
}
}
/**
* MMLEventリストのロールを表示します。
* @param g
* @param mmlPart
*/
private void paintMMLPart(Graphics2D g, List<MMLNoteEvent> mmlPart, Color rectColor, Color fillColor, boolean drawOption) {
MMLNoteEvent prevNote = new MMLNoteEvent(0, 0, 0, MMLNoteEvent.INIT_VOL);
// 現在のView範囲のみを描画する.
for (MMLNoteEvent noteEvent : mmlPart) {
if ( (noteEvent.getEndTick() < startViewTick) && (noteEvent.getTickOffset() < startViewTick - DRAW_START_MARGIN) ) {
prevNote = noteEvent;
continue;
}
if (noteEvent.getTickOffset() > endViewTick) {
break;
}
drawNote(g, noteEvent, rectColor, fillColor, drawOption, prevNote);
prevNote = noteEvent;
}
}
/**
* 1トラック分のロールを表示します。(アクティブトラックは表示しない)
* @param g
* @param index トラックindex
*/
private void paintMMLTrack(Graphics2D g, int index, MMLTrack track) {
boolean instEnable[] = InstClass.getEnablePartByProgram(track.getProgram());
boolean songExEnable[] = InstClass.getEnablePartByProgram(track.getSongProgram());
MMLEventList activePart = mmlManager.getActiveMMLPart();
int colorIndex = 0;
for (int i = 0; i < track.getMMLEventList().size(); i++) {
MMLEventList targetPart = track.getMMLEventAtIndex(i);
if (targetPart == activePart) {
colorIndex++;
continue;
}
Color rectColor = ColorManager.defaultColor().getPartRectColor(index, colorIndex);
Color fillColor = ColorManager.defaultColor().getPartFillColor(index, colorIndex);
if ( !instEnable[i] && !songExEnable[i] ) {
fillColor = ColorManager.defaultColor().getUnusedFillColor();
} else {
colorIndex++;
}
paintMMLPart(g, track.getMMLEventList().get(i).getMMLNoteEventList(), rectColor, fillColor, false);
}
}
private void paintActiveTrack(Graphics2D g) {
int trackIndex = mmlManager.getActiveTrackIndex();
if (paintMode != PaintMode.ACTIVE_PART) {
paintMMLTrack(g, trackIndex, mmlManager.getMMLScore().getTrack(trackIndex));
}
MMLEventList activePart = mmlManager.getActiveMMLPart();
if (activePart != null) {
Color rectColor = ColorManager.defaultColor().getActiveRectColor(trackIndex);
Color fillColor = ColorManager.defaultColor().getActiveFillColor(trackIndex);
paintMMLPart(g, activePart.getMMLNoteEventList(), rectColor, fillColor, true);
}
}
private void paintOtherTrack(Graphics2D g) {
MMLScore mmlScore = mmlManager.getMMLScore();
if (paintMode != PaintMode.ALL_TRACK) {
return;
}
if (mmlScore != null) {
for (int i = 0; i < mmlScore.getTrackCount(); i++) {
MMLTrack track = mmlScore.getTrack(i);
if (track != mmlScore.getTrack(mmlManager.getActiveTrackIndex())) {
paintMMLTrack(g, i, track);
}
}
}
}
private void paintSelectedNote(Graphics2D g) {
// 選択中ノートの表示
if (selectNoteList != null) {
paintMMLPart(g, selectNoteList, Color.YELLOW, Color.YELLOW, false);
}
}
private void paintSelectingArea(Graphics2D g) {
if (selectingRect != null) {
g.setColor(Color.BLUE);
g.draw(selectingRect);
}
}
public void setPitchRange(InstClass inst) {
if (inst == null) {
return;
}
this.lowerNote = inst.getLowerNote();
this.upperNote = inst.getUpperNote();
}
}