OSDN Git Service

・パッケージ名を付加
[midichordhelper/MIDIChordHelper.git] / src / camidion / chordhelper / pianokeyboard / PianoKeyboard.java
1 package camidion.chordhelper.pianokeyboard;\r
2 \r
3 import java.awt.BorderLayout;\r
4 import java.awt.Color;\r
5 import java.awt.Dimension;\r
6 import java.awt.Graphics;\r
7 import java.awt.Graphics2D;\r
8 import java.awt.Point;\r
9 import java.awt.Rectangle;\r
10 import java.awt.event.ComponentAdapter;\r
11 import java.awt.event.ComponentEvent;\r
12 import java.awt.event.FocusEvent;\r
13 import java.awt.event.FocusListener;\r
14 import java.awt.event.InputEvent;\r
15 import java.awt.event.KeyEvent;\r
16 import java.awt.event.KeyListener;\r
17 import java.awt.event.MouseEvent;\r
18 import java.awt.event.MouseListener;\r
19 import java.awt.event.MouseMotionListener;\r
20 import java.util.LinkedList;\r
21 import java.util.Vector;\r
22 \r
23 import javax.sound.midi.MidiChannel;\r
24 import javax.swing.BoundedRangeModel;\r
25 import javax.swing.DefaultBoundedRangeModel;\r
26 import javax.swing.JComponent;\r
27 import javax.swing.event.ChangeEvent;\r
28 import javax.swing.event.ChangeListener;\r
29 import javax.swing.event.ListDataEvent;\r
30 import javax.swing.event.ListDataListener;\r
31 \r
32 import camidion.chordhelper.ChordDisplayLabel;\r
33 import camidion.chordhelper.anogakki.AnoGakkiPane;\r
34 import camidion.chordhelper.chordmatrix.ChordMatrix;\r
35 import camidion.chordhelper.mididevice.AbstractMidiChannelStatus;\r
36 import camidion.chordhelper.mididevice.AbstractMidiStatus;\r
37 import camidion.chordhelper.mididevice.AbstractVirtualMidiDevice;\r
38 import camidion.chordhelper.mididevice.VirtualMidiDevice;\r
39 import camidion.chordhelper.midieditor.DefaultMidiChannelComboBoxModel;\r
40 import camidion.chordhelper.midieditor.MidiChannelButtonSelecter;\r
41 import camidion.chordhelper.music.Chord;\r
42 import camidion.chordhelper.music.Key;\r
43 import camidion.chordhelper.music.MIDISpec;\r
44 import camidion.chordhelper.music.Music;\r
45 \r
46 /**\r
47  * Piano Keyboard class for MIDI Chord Helper\r
48  *\r
49  * @author\r
50  *      Copyright (C) 2004-2013 Akiyoshi Kamide\r
51  *      http://www.yk.rim.or.jp/~kamide/music/chordhelper/\r
52  */\r
53 public class PianoKeyboard extends JComponent {\r
54         /**\r
55          * 最小オクターブ幅\r
56          */\r
57         public static final int MIN_OCTAVES = 3;\r
58         /**\r
59          * 最大オクターブ幅(切り上げ)\r
60          */\r
61         public static final int MAX_OCTAVES = MIDISpec.MAX_NOTE_NO / 12 + 1;\r
62         /**\r
63          * 濃いピンク\r
64          */\r
65         public static final Color DARK_PINK = new Color(0xFF,0x50,0x80);\r
66 \r
67         /** 1オクターブあたりの幅 */\r
68         private static float WIDTH_PER_OCTAVE = Music.SEMITONES_PER_OCTAVE * 10;\r
69         /** 白鍵のサイズ */\r
70         private Dimension       whiteKeySize;\r
71         /** 黒鍵のサイズ */\r
72         private Dimension       blackKeySize;\r
73         /** ダークモードならtrue */\r
74         boolean         isDark = false;\r
75 \r
76         /** すべてのピアノキー */\r
77         private PianoKey[] keys;\r
78         /** 黒鍵 */\r
79         private PianoKey[] blackKeys;\r
80         /** 白鍵 */\r
81         private PianoKey[] whiteKeys;\r
82         /**\r
83          * オクターブ範囲モデル\r
84          */\r
85         public BoundedRangeModel octaveRangeModel;\r
86         /**\r
87          * オクターブ幅モデル\r
88          */\r
89         public BoundedRangeModel octaveSizeModel;\r
90         /**\r
91          * ベロシティモデル\r
92          */\r
93         public BoundedRangeModel velocityModel = new DefaultBoundedRangeModel(64, 0, 0, 127);\r
94         /**\r
95          * MIDIチャンネル選択コンボボックスモデル\r
96          */\r
97         public DefaultMidiChannelComboBoxModel\r
98                 midiChComboboxModel = new DefaultMidiChannelComboBoxModel();\r
99 \r
100         /**\r
101          * ノートのリスト。配列の要素として使えるようクラス名を割り当てます。\r
102          */\r
103         private class NoteList extends LinkedList<Integer> {\r
104                 // 何もすることはない\r
105         }\r
106         /**\r
107          * 選択マーク●がついている鍵を表すノートリスト\r
108          */\r
109         private NoteList selectedKeyNoteList = new NoteList();\r
110         /**\r
111          * 調号(スケール判定用)\r
112          */\r
113         private Key     keySignature = null;\r
114         /**\r
115          * 表示中のコード\r
116          */\r
117         private Chord   chord = null;\r
118         /**\r
119          * コードボタンマトリクス\r
120          */\r
121         public ChordMatrix chordMatrix;\r
122         /**\r
123          * コード表示部\r
124          */\r
125         public ChordDisplayLabel chordDisplay;\r
126         /**\r
127          * Innocence「あの楽器」\r
128          */\r
129         public AnoGakkiPane anoGakkiPane;\r
130         /**\r
131          * MIDIチャンネルをボタンで選択\r
132          */\r
133         public MidiChannelButtonSelecter midiChannelButtonSelecter;\r
134 \r
135         private NoteList[] channelNotes = new NoteList[MIDISpec.MAX_CHANNELS];\r
136         private int[] pitchBendValues = new int[MIDISpec.MAX_CHANNELS];\r
137         private int[] pitchBendSensitivities = new int[MIDISpec.MAX_CHANNELS];\r
138         private int[] modulations = new int[MIDISpec.MAX_CHANNELS];\r
139 \r
140         private class MidiChannelStatus extends AbstractMidiChannelStatus {\r
141                 public MidiChannelStatus(int channel) {\r
142                         super(channel);\r
143                         channelNotes[channel] = new NoteList();\r
144                         pitchBendSensitivities[channel] = 2; // Default is wholetone = 2 semitones\r
145                 }\r
146                 @Override\r
147                 public void fireRpnChanged() {\r
148                         if( dataFor != DATA_FOR_RPN ) return;\r
149 \r
150                         // RPN (MSB) - Accept 0x00 only\r
151                         if( controllerValues[0x65] != 0x00 ) return;\r
152 \r
153                         // RPN (LSB)\r
154                         switch( controllerValues[0x64] ) {\r
155                         case 0x00: // Pitch Bend Sensitivity\r
156                                 if( controllerValues[0x06] == 0 ) return;\r
157                                 pitchBendSensitivities[channel] = controllerValues[0x06];\r
158                                 break;\r
159                         }\r
160                 }\r
161                 @Override\r
162                 public void noteOff(int noteNumber, int velocity) {\r
163                         noteOff(noteNumber);\r
164                 }\r
165                 @Override\r
166                 public void noteOff(int noteNumber) {\r
167                         keyOff( channel, noteNumber );\r
168                         if( chordMatrix != null ) {\r
169                                 if( ! isRhythmPart() )\r
170                                         chordMatrix.note(false, noteNumber);\r
171                         }\r
172                         if( midiChannelButtonSelecter != null ) {\r
173                                 midiChannelButtonSelecter.repaint();\r
174                         }\r
175                 }\r
176                 @Override\r
177                 public void noteOn(int noteNumber, int velocity) {\r
178                         if( velocity <= 0 ) {\r
179                                 noteOff(noteNumber); return;\r
180                         }\r
181                         keyOn( channel, noteNumber );\r
182                         if( midiChComboboxModel.getSelectedChannel() == channel ) {\r
183                                 if( chordDisplay != null ) {\r
184                                         if( chordMatrix != null && chordMatrix.isPlaying() )\r
185                                                 chordDisplay.clear();\r
186                                         else\r
187                                                 chordDisplay.setNote(noteNumber, isRhythmPart());\r
188                                 }\r
189                                 if( anoGakkiPane != null ) {\r
190                                         PianoKey piano_key = getPianoKey(noteNumber);\r
191                                         if( piano_key != null )\r
192                                                 anoGakkiPane.start(PianoKeyboard.this, piano_key.indicator);\r
193                                 }\r
194                         }\r
195                         if( chordMatrix != null ) {\r
196                                 if( ! isRhythmPart() )\r
197                                         chordMatrix.note(true, noteNumber);\r
198                         }\r
199                         if( midiChannelButtonSelecter != null ) {\r
200                                 midiChannelButtonSelecter.repaint();\r
201                         }\r
202                 }\r
203                 @Override\r
204                 public void allNotesOff() {\r
205                         allKeysOff( channel, -1 );\r
206                         if( chordMatrix != null )\r
207                                 chordMatrix.clearIndicators();\r
208                 }\r
209                 @Override\r
210                 public void setPitchBend(int bend) {\r
211                         super.setPitchBend(bend);\r
212                         pitchBendValues[channel] = bend;\r
213                         repaintNotes();\r
214                 }\r
215                 @Override\r
216                 public void resetAllControllers() {\r
217                         super.resetAllControllers();\r
218                         //\r
219                         // See also: Response to Reset All Controllers\r
220                         //     http://www.midi.org/about-midi/rp15.shtml\r
221                         //\r
222                         pitchBendValues[channel] = MIDISpec.PITCH_BEND_NONE;\r
223                         modulations[channel] = 0;\r
224                         repaintNotes();\r
225                 }\r
226                 @Override\r
227                 public void controlChange(int controller, int value) {\r
228                         super.controlChange(controller,value);\r
229                         switch( controller ) {\r
230                         case 0x01: // Moduration (MSB)\r
231                                 modulations[channel] = value;\r
232                                 repaintNotes();\r
233                                 break;\r
234                         }\r
235                 }\r
236                 private void repaintNotes() {\r
237                         if( midiChComboboxModel.getSelectedChannel() != channel\r
238                                 || channelNotes[channel] == null\r
239                         )\r
240                                 return;\r
241                         if( channelNotes[channel].size() > 0 || selectedKeyNoteList.size() > 0 )\r
242                                 repaint();\r
243                 }\r
244         }\r
245         /**\r
246          * この鍵盤の仮想MIDIデバイスです。\r
247          * ノートオンなどのMIDIメッセージを受け取り、画面に反映します。\r
248          */\r
249         public VirtualMidiDevice midiDevice = new AbstractVirtualMidiDevice() {\r
250                 class MyInfo extends Info {\r
251                         protected MyInfo() {\r
252                                 super("MIDI Keyboard","Unknown vendor","Software MIDI keyboard","");\r
253                         }\r
254                 }\r
255                 /**\r
256                  * MIDIデバイス情報\r
257                  */\r
258                 protected MyInfo info;\r
259                 @Override\r
260                 public Info getDeviceInfo() { return info; }\r
261                 {\r
262                         info = new MyInfo();\r
263                         // 受信してMIDIチャンネルの状態を管理する\r
264                         setReceiver(\r
265                                 new AbstractMidiStatus() {{\r
266                                         for( int i=0; i<MIDISpec.MAX_CHANNELS; i++ )\r
267                                                 add(new MidiChannelStatus(i));\r
268                                 }}\r
269                         );\r
270                 }\r
271         };\r
272 \r
273         /**\r
274          * 現在選択中のMIDIチャンネルを返します。\r
275          * @return 現在選択中のMIDIチャンネル\r
276          */\r
277         public MidiChannel getSelectedChannel() {\r
278                 return midiDevice.getChannels()[midiChComboboxModel.getSelectedChannel()];\r
279         }\r
280         /**\r
281          * 現在選択中のMIDIチャンネルにノートオンメッセージを送出します。\r
282          * ベロシティ値は現在画面で設定中の値となります。\r
283          * @param noteNumber ノート番号\r
284          */\r
285         public void noteOn(int noteNumber) {\r
286                 getSelectedChannel().noteOn(noteNumber, velocityModel.getValue());\r
287         }\r
288         /**\r
289          * 現在選択中のMIDIチャンネルにノートオフメッセージを送出します。\r
290          * ベロシティ値は現在画面で設定中の値となります。\r
291          * @param noteNumber ノート番号\r
292          */\r
293         public void noteOff(int noteNumber) {\r
294                 getSelectedChannel().noteOff(noteNumber, velocityModel.getValue());\r
295         }\r
296 \r
297         /**\r
298          * 1個のピアノ鍵盤を表す矩形\r
299          */\r
300         private class PianoKey extends Rectangle {\r
301                 private boolean isBlack = false;\r
302                 private int position = 0;\r
303                 private String bindedKeyChar = null;\r
304                 private Rectangle indicator;\r
305                 private boolean outOfBounds = false;\r
306                 public PianoKey(Point p, Dimension d, Dimension indicatorSize) {\r
307                         super(p,d);\r
308                         Point indicatorPosition = new Point(\r
309                                 p.x + (d.width - indicatorSize.width) / 2,\r
310                                 p.y + d.height - indicatorSize.height - indicatorSize.height / 2 + 2\r
311                         );\r
312                         indicator = new Rectangle(indicatorPosition, indicatorSize);\r
313                 }\r
314                 int getNote(int chromaticOffset) {\r
315                         int n = position + chromaticOffset;\r
316                         return (outOfBounds = ( n > MIDISpec.MAX_NOTE_NO )) ? -1 : n;\r
317                 }\r
318                 boolean paintKey(Graphics2D g2, boolean isPressed) {\r
319                         if(outOfBounds) return false;\r
320                         g2.fill3DRect(x, y, width, height, !isPressed);\r
321                         return true;\r
322                 }\r
323                 boolean paintKey(Graphics2D g2) {\r
324                         return paintKey(g2,false);\r
325                 }\r
326                 boolean paintKeyBinding(Graphics2D g2) {\r
327                         if( bindedKeyChar == null ) return false;\r
328                         g2.drawString( bindedKeyChar, x + width/3, indicator.y - 2 );\r
329                         return true;\r
330                 }\r
331                 boolean paintIndicator(Graphics2D g2, boolean is_small, int pitch_bend_value) {\r
332                         if( is_small ) {\r
333                                 g2.fillOval(\r
334                                         indicator.x + indicator.width/4,\r
335                                         indicator.y + indicator.height/4 + 1,\r
336                                         indicator.width/2,\r
337                                         indicator.height/2\r
338                                 );\r
339                         }\r
340                         else {\r
341                                 int current_channel = midiChComboboxModel.getSelectedChannel();\r
342                                 int sens = pitchBendSensitivities[current_channel];\r
343                                 if( sens == 0 ) {\r
344                                         sens = 2;\r
345                                 }\r
346                                 int x_offset = (\r
347                                         7 * whiteKeySize.width * sens * (pitch_bend_value - MIDISpec.PITCH_BEND_NONE)\r
348                                 ) / (12 * 8192);\r
349                                 int additional_height = indicator.height * modulations[current_channel] / 256 ;\r
350                                 int y_offset = additional_height / 2 ;\r
351                                 g2.fillOval(\r
352                                         indicator.x + ( x_offset < 0 ? x_offset : 0 ),\r
353                                         indicator.y - y_offset,\r
354                                         indicator.width + ( x_offset < 0 ? -x_offset : x_offset ),\r
355                                         indicator.height + additional_height\r
356                                 );\r
357                         }\r
358                         return true;\r
359                 }\r
360                 boolean paintIndicator(Graphics2D g2, boolean is_small) {\r
361                         return paintIndicator( g2, is_small, 0 );\r
362                 }\r
363         }\r
364 \r
365         private class MouseKeyListener\r
366                 implements MouseListener, MouseMotionListener, KeyListener\r
367         {\r
368                 private void pressed(int c, int n, InputEvent e) {\r
369                         keyOn(c,n);\r
370                         noteOn(n);\r
371                         firePianoKeyPressed(n,e);\r
372                 }\r
373                 private void released(int c, int n, InputEvent e) {\r
374                         keyOff(c,n);\r
375                         noteOff(n);\r
376                         firePianoKeyReleased(n,e);\r
377                 }\r
378                 @Override\r
379                 public void mousePressed(MouseEvent e) {\r
380                         int n = getNote(e.getPoint());\r
381                         if( n < 0 ) return;\r
382                         int c = midiChComboboxModel.getSelectedChannel();\r
383                         if( channelNotes[c].contains(n) ) return;\r
384                         chord = null;\r
385                         pressed(c,n,e);\r
386                         requestFocusInWindow();\r
387                         repaint();\r
388                 }\r
389                 @Override\r
390                 public void mouseReleased(MouseEvent e) {\r
391                         int c = midiChComboboxModel.getSelectedChannel();\r
392                         NoteList nl = channelNotes[c];\r
393                         if( ! nl.isEmpty() )\r
394                                 released(c, nl.poll(), e);\r
395                 }\r
396                 @Override\r
397                 public void mouseEntered(MouseEvent e) {\r
398                 }\r
399                 @Override\r
400                 public void mouseExited(MouseEvent e) {\r
401                 }\r
402                 @Override\r
403                 public void mouseDragged(MouseEvent e) {\r
404                         int n = getNote(e.getPoint());\r
405                         if( n < 0 ) return;\r
406                         int c = midiChComboboxModel.getSelectedChannel();\r
407                         NoteList nl = channelNotes[c];\r
408                         if( nl.contains(n) ) return;\r
409                         if( ! nl.isEmpty() )\r
410                                 released(c, nl.poll(), e);\r
411                         pressed(c,n,e);\r
412                 }\r
413                 @Override\r
414                 public void mouseMoved(MouseEvent e) {\r
415                 }\r
416                 @Override\r
417                 public void mouseClicked(MouseEvent e) {\r
418                 }\r
419                 @Override\r
420                 public void keyPressed(KeyEvent e) {\r
421                         int kc = e.getKeyCode();\r
422                         if( kc == KeyEvent.VK_LEFT || kc == KeyEvent.VK_KP_LEFT ) {\r
423                                 octaveRangeModel.setValue( octaveRangeModel.getValue() - 1 );\r
424                                 return;\r
425                         }\r
426                         else if( kc == KeyEvent.VK_RIGHT || kc == KeyEvent.VK_KP_RIGHT ) {\r
427                                 octaveRangeModel.setValue( octaveRangeModel.getValue() + 1 );\r
428                                 return;\r
429                         }\r
430                         int n = getNote(e); if( n < 0 ) return;\r
431                         int c = midiChComboboxModel.getSelectedChannel();\r
432                         if( channelNotes[c].contains(n) ) return;\r
433                         chord = null;\r
434                         pressed(c,n,e);\r
435                 }\r
436                 @Override\r
437                 public void keyReleased(KeyEvent e) {\r
438                         int c = midiChComboboxModel.getSelectedChannel();\r
439                         int n = getNote(e);\r
440                         if( n < 0 || ! channelNotes[c].contains(n) ) return;\r
441                         released(c,n,e);\r
442                 }\r
443                 @Override\r
444                 public void keyTyped(KeyEvent e) {\r
445                 }\r
446         }\r
447 \r
448         /**\r
449          * 新しいピアノキーボードを構築します。\r
450          */\r
451         public PianoKeyboard() {\r
452                 setLayout(new BorderLayout());\r
453                 setFocusable(true);\r
454                 addFocusListener(new FocusListener() {\r
455                         public void focusGained(FocusEvent e) { repaint(); }\r
456                         public void focusLost(FocusEvent e)   { repaint(); }\r
457                 });\r
458                 MouseKeyListener mkl = new MouseKeyListener();\r
459                 addMouseListener(mkl);\r
460                 addMouseMotionListener(mkl);\r
461                 addKeyListener(mkl);\r
462                 int octaves = getPerferredOctaves();\r
463                 octaveSizeModel = new DefaultBoundedRangeModel(\r
464                         octaves, 0, MIN_OCTAVES, MAX_OCTAVES\r
465                 ) {{\r
466                         addChangeListener(new ChangeListener() {\r
467                                 public void stateChanged(ChangeEvent e) {\r
468                                         fireOctaveResized(e);\r
469                                         octaveSizeChanged();\r
470                                 }\r
471                         });\r
472                 }};\r
473                 octaveRangeModel = new DefaultBoundedRangeModel(\r
474                         (MAX_OCTAVES - octaves) / 2, octaves, 0, MAX_OCTAVES\r
475                 ) {{\r
476                         addChangeListener(new ChangeListener() {\r
477                                 public void stateChanged(ChangeEvent e) {\r
478                                         fireOctaveMoved(e);\r
479                                         checkOutOfBounds();\r
480                                         repaint();\r
481                                 }\r
482                         });\r
483                 }};\r
484                 addComponentListener(new ComponentAdapter() {\r
485                         @Override\r
486                         public void componentResized(ComponentEvent e) {\r
487                                 octaveSizeModel.setValue( getPerferredOctaves() );\r
488                                 octaveSizeChanged();\r
489                         }\r
490                 });\r
491                 midiChComboboxModel.addListDataListener(\r
492                         new ListDataListener() {\r
493                                 public void contentsChanged(ListDataEvent e) {\r
494                                         int c = midiChComboboxModel.getSelectedChannel();\r
495                                         for( int n : channelNotes[c] )\r
496                                                 if( autoScroll(n) ) break;\r
497                                         repaint();\r
498                                 }\r
499                                 public void intervalAdded(ListDataEvent e) {}\r
500                                 public void intervalRemoved(ListDataEvent e) {}\r
501                         }\r
502                 );\r
503         }\r
504         public void paint(Graphics g) {\r
505                 if( keys == null ) return;\r
506                 Graphics2D g2 = (Graphics2D) g;\r
507                 Dimension d = getSize();\r
508                 //\r
509                 // 鍵盤をクリア\r
510                 g2.setBackground( getBackground() );\r
511                 g2.clearRect( 0, 0, d.width, d.height );\r
512                 //\r
513                 // 白鍵を描画\r
514                 g2.setColor( isDark ? Color.gray : Color.white );\r
515                 for( PianoKey k : whiteKeys ) k.paintKey(g2);\r
516 \r
517                 NoteList notesArray[] = {\r
518                         (NoteList)selectedKeyNoteList.clone(),\r
519                         (NoteList)channelNotes[midiChComboboxModel.getSelectedChannel()].clone()\r
520                 };\r
521                 PianoKey key;\r
522                 //\r
523                 // ノートオン状態の白鍵を塗り重ねる\r
524                 for( int n : notesArray[1] )\r
525                         if( (key=getPianoKey(n)) != null && !(key.isBlack) )\r
526                                 key.paintKey(g2,true);\r
527                 //\r
528                 // 黒鍵を描画\r
529                 g2.setColor(getForeground());\r
530                 for( PianoKey k : blackKeys ) k.paintKey(g2);\r
531                 //\r
532                 // ノートオン状態の黒鍵を塗り重ねる\r
533                 g2.setColor( Color.gray );\r
534                 for( int n : notesArray[1] )\r
535                         if( (key=getPianoKey(n)) != null && key.isBlack )\r
536                                 key.paintKey(g2,true);\r
537                 //\r
538                 // インジケータの表示\r
539                 for( NoteList nl : notesArray ) {\r
540                         if( nl == null ) continue;\r
541                         for( Integer ni : nl ) {\r
542                                 if( ni == null ) continue;\r
543                                 int n = ni;\r
544                                 if( (key=getPianoKey(n)) == null ) continue;\r
545                                 boolean isOnScale = (keySignature == null || keySignature.isOnScale(n));\r
546                                 int chordIndex;\r
547                                 if( chord != null && (chordIndex = chord.indexOf(n)) >=0 ) {\r
548                                         g2.setColor(Chord.NOTE_INDEX_COLORS[chordIndex]);\r
549                                 }\r
550                                 else {\r
551                                         g2.setColor(isDark && isOnScale ? Color.pink : DARK_PINK);\r
552                                 }\r
553                                 int c = midiChComboboxModel.getSelectedChannel();\r
554                                 key.paintIndicator(g2, false, pitchBendValues[c]);\r
555                                 if( ! isOnScale ) {\r
556                                         g2.setColor(Color.white);\r
557                                         key.paintIndicator(g2, true);\r
558                                 }\r
559                         }\r
560                 }\r
561                 if( isFocusOwner() ) {\r
562                         // Show PC-key binding\r
563                         for( PianoKey k : bindedKeys ) {\r
564                                 g2.setColor(\r
565                                         k.isBlack ? Color.gray.brighter() :\r
566                                         isDark ? getForeground() :\r
567                                         getForeground().brighter()\r
568                                 );\r
569                                 k.paintKeyBinding(g2);\r
570                         }\r
571                 }\r
572         }\r
573         //\r
574         protected void firePianoKeyPressed(int note_no, InputEvent event) {\r
575                 Object[] listeners = listenerList.getListenerList();\r
576                 for (int i = listeners.length-2; i>=0; i-=2) {\r
577                         if (listeners[i]==PianoKeyboardListener.class) {\r
578                                 ((PianoKeyboardListener)listeners[i+1]).pianoKeyPressed(note_no,event);\r
579                         }\r
580                 }\r
581         }\r
582         protected void firePianoKeyReleased(int note_no, InputEvent event) {\r
583                 Object[] listeners = listenerList.getListenerList();\r
584                 for (int i = listeners.length-2; i>=0; i-=2) {\r
585                         if (listeners[i]==PianoKeyboardListener.class) {\r
586                                 ((PianoKeyboardListener)listeners[i+1]).pianoKeyReleased(note_no,event);\r
587                         }\r
588                 }\r
589         }\r
590         protected void fireOctaveMoved(ChangeEvent event) {\r
591                 Object[] listeners = listenerList.getListenerList();\r
592                 for (int i = listeners.length-2; i>=0; i-=2) {\r
593                         if (listeners[i]==PianoKeyboardListener.class) {\r
594                                 ((PianoKeyboardListener)listeners[i+1]).octaveMoved(event);\r
595                         }\r
596                 }\r
597         }\r
598         protected void fireOctaveResized(ChangeEvent event) {\r
599                 Object[] listeners = listenerList.getListenerList();\r
600                 for (int i = listeners.length-2; i>=0; i-=2) {\r
601                         if (listeners[i]==PianoKeyboardListener.class) {\r
602                                 ((PianoKeyboardListener)listeners[i+1]).octaveResized(event);\r
603                         }\r
604                 }\r
605         }\r
606         /**\r
607          * 現在のオクターブ位置における、\r
608          * 指定のノート番号に対する1個のピアノキーを返します。\r
609          * @param noteNumber ノート番号\r
610          * @return ピアノキー(範囲外の場合 null)\r
611          */\r
612         private PianoKey getPianoKey(int noteNumber) {\r
613                 int i = noteNumber - octaveRangeModel.getValue() * 12 ;\r
614                 return i>=0 && i<keys.length ? keys[i]: null;\r
615         }\r
616         /**\r
617          * 指定の座標におけるノート番号を返します。\r
618          * @param point 座標\r
619          * @return ノート番号(範囲外の場合 -1)\r
620          */\r
621         private int getNote(Point point) {\r
622                 PianoKey k = getPianoKeyAt(point);\r
623                 return k==null ? -1 : k.getNote(getChromaticOffset());\r
624         }\r
625         /**\r
626          * 指定の座標における1個のピアノキーを返します。\r
627          * @param point 座標\r
628          * @return ピアノキー(範囲外の場合 null)\r
629          */\r
630         private PianoKey getPianoKeyAt(Point point) {\r
631                 int indexWhite = point.x / whiteKeySize.width;\r
632                 int indexOctave = indexWhite / 7;\r
633                 int i = (indexWhite -= indexOctave * 7) * 2 + indexOctave * 12;\r
634                 if( indexWhite >= 3 ) i--;\r
635                 if( i < 0 || i > keys.length-1 )\r
636                         return null;\r
637                 if( point.y > blackKeySize.height )\r
638                         return keys[i];\r
639                 PianoKey k;\r
640                 if( i > 0 ) {\r
641                         k = keys[i-1];\r
642                         if( k.isBlack && !(k.outOfBounds) && k.contains(point) )\r
643                                 return k;\r
644                 }\r
645                 if( i < keys.length-1 ) {\r
646                         k = keys[i+1];\r
647                         if( k.isBlack && !(k.outOfBounds) && k.contains(point) )\r
648                                 return k;\r
649                 }\r
650                 return keys[i];\r
651         }\r
652 \r
653         private PianoKey[] bindedKeys;\r
654         private int             bindedKeyPosition;\r
655         private String  bindedKeyChars;\r
656         private PianoKey getPianoKey(KeyEvent e) {\r
657                 int i = bindedKeyChars.indexOf(e.getKeyChar());\r
658                 return i >= 0 ? keys[bindedKeyPosition + i] : null;\r
659         }\r
660         private int getNote(KeyEvent e) {\r
661                 PianoKey k = getPianoKey(e);\r
662                 return k==null ? -1 : k.getNote(getChromaticOffset());\r
663         }\r
664         private void changeKeyBinding(int from, String keyChars) {\r
665                 PianoKey k;\r
666                 bindedKeys = new PianoKey[(bindedKeyChars = keyChars).length()];\r
667                 bindedKeyPosition = from;\r
668                 for( int i = 0; i < bindedKeyChars.length(); i++ ) {\r
669                         bindedKeys[i] = k = keys[ bindedKeyPosition + i ];\r
670                         k.bindedKeyChar = bindedKeyChars.substring( i, i+1 );\r
671                 }\r
672                 repaint();\r
673         }\r
674 \r
675         private void checkOutOfBounds() {\r
676                 if( keys == null ) return;\r
677                 for( PianoKey k : keys ) k.getNote(getChromaticOffset());\r
678         }\r
679         private void keyOff(int ch, int noteNumber) {\r
680                 if( noteNumber < 0 || ch < 0 || ch >= channelNotes.length ) return;\r
681                 channelNotes[ch].remove((Object)noteNumber);\r
682                 if( ch == midiChComboboxModel.getSelectedChannel() )\r
683                         repaint();\r
684         }\r
685         private void keyOn(int ch, int noteNumber) {\r
686                 if( noteNumber < 0 || ch < 0 || ch >= channelNotes.length ) return;\r
687                 channelNotes[ch].add(noteNumber);\r
688                 setSelectedNote(ch,noteNumber);\r
689         }\r
690         public boolean autoScroll(int noteNumber) {\r
691                 if( octaveRangeModel == null || keys == null )\r
692                         return false;\r
693                 int i = noteNumber - getChromaticOffset();\r
694                 if( i < 0 ) {\r
695                         octaveRangeModel.setValue(\r
696                                 octaveRangeModel.getValue() - (-i)/Music.SEMITONES_PER_OCTAVE - 1\r
697                         );\r
698                         return true;\r
699                 }\r
700                 if( i >= keys.length ) {\r
701                         octaveRangeModel.setValue(\r
702                                 octaveRangeModel.getValue() + (i-keys.length)/Music.SEMITONES_PER_OCTAVE + 1\r
703                         );\r
704                         return true;\r
705                 }\r
706                 return false;\r
707         }\r
708         public void addPianoKeyboardListener(PianoKeyboardListener l) {\r
709                 listenerList.add(PianoKeyboardListener.class, l);\r
710         }\r
711         public void removePianoKeyboardListener(PianoKeyboardListener l) {\r
712                 listenerList.remove(PianoKeyboardListener.class, l);\r
713         }\r
714         int countKeyOn() {\r
715                 return channelNotes[midiChComboboxModel.getSelectedChannel()].size();\r
716         }\r
717         public int countKeyOn(int ch) {\r
718                 return channelNotes[ch].size();\r
719         }\r
720         void allKeysOff(int ch, int numMarks) {\r
721                 if( ! selectedKeyNoteList.isEmpty() ) return;\r
722                 switch(numMarks) {\r
723                 case -1:\r
724                         selectedKeyNoteList = (NoteList)(channelNotes[ch].clone());\r
725                         break;\r
726                 case  1:\r
727                         selectedKeyNoteList.add(\r
728                                 channelNotes[ch].get(channelNotes[ch].size()-1)\r
729                         );\r
730                         break;\r
731                 default: break;\r
732                 }\r
733                 channelNotes[ch].clear();\r
734                 if( midiChComboboxModel.getSelectedChannel() == ch )\r
735                         repaint();\r
736         }\r
737         public void clear() {\r
738                 selectedKeyNoteList.clear();\r
739                 channelNotes[midiChComboboxModel.getSelectedChannel()].clear();\r
740                 chord = null;\r
741                 repaint();\r
742         }\r
743         int getNote() {\r
744                 int current_channel = midiChComboboxModel.getSelectedChannel();\r
745                 switch( channelNotes[current_channel].size() ) {\r
746                 case 1: return channelNotes[current_channel].get(0);\r
747                 case 0:\r
748                         if( selectedKeyNoteList.size() == 1 )\r
749                                 return selectedKeyNoteList.get(0);\r
750                         return -1;\r
751                 default:\r
752                         return -1;\r
753                 }\r
754         }\r
755         public void setSelectedNote(int noteNumber) {\r
756                 setSelectedNote(midiChComboboxModel.getSelectedChannel(), noteNumber);\r
757         }\r
758         void setSelectedNote(int ch, int note_no) {\r
759                 if( ch != midiChComboboxModel.getSelectedChannel() )\r
760                         return;\r
761                 selectedKeyNoteList.add(note_no);\r
762                 int maxSel = (chord == null ? maxSelectable : chord.numberOfNotes());\r
763                 while( selectedKeyNoteList.size() > maxSel )\r
764                         selectedKeyNoteList.poll();\r
765                 if( !autoScroll(note_no) ) {\r
766                         // When autoScroll() returned false, stateChanged() not invoked - need repaint()\r
767                         repaint();\r
768                 }\r
769         }\r
770         public Integer[] getSelectedNotes() {\r
771                 return selectedKeyNoteList.toArray(new Integer[0]);\r
772         }\r
773         Chord getChord() { return chord; }\r
774         public void setChord(Chord c) {\r
775                 chordDisplay.setChord(chord = c);\r
776         }\r
777         public void setKeySignature(Key ks) {\r
778                 keySignature = ks;\r
779                 repaint();\r
780         }\r
781         private int     maxSelectable = 1;\r
782         public void setMaxSelectable( int maxSelectable ) {\r
783                 this.maxSelectable = maxSelectable;\r
784         }\r
785         int getMaxSelectable() { return maxSelectable; }\r
786         public int getChromaticOffset() {\r
787                 return octaveRangeModel.getValue() * Music.SEMITONES_PER_OCTAVE ;\r
788         }\r
789         public int getOctaves() { return octaveSizeModel.getValue(); }\r
790         private int getPerferredOctaves() {\r
791                 int octaves = Math.round( (float)getWidth() / WIDTH_PER_OCTAVE );\r
792                 if( octaves > MAX_OCTAVES ) {\r
793                         octaves = MAX_OCTAVES;\r
794                 }\r
795                 else if( octaves < MIN_OCTAVES ) {\r
796                         octaves = MIN_OCTAVES;\r
797                 }\r
798                 return octaves;\r
799         }\r
800         private void octaveSizeChanged() {\r
801                 int octaves = octaveSizeModel.getValue();\r
802                 String defaultBindedKeyChars = "zsxdcvgbhnjm,l.;/\\]";\r
803                 Dimension keyboard_size = getSize();\r
804                 if( keyboard_size.width == 0 ) {\r
805                         return;\r
806                 }\r
807                 whiteKeySize = new Dimension(\r
808                         (keyboard_size.width - 1) / (octaves * 7 + 1),\r
809                         keyboard_size.height - 1\r
810                 );\r
811                 blackKeySize = new Dimension(\r
812                         whiteKeySize.width * 3 / 4,\r
813                         whiteKeySize.height * 3 / 5\r
814                 );\r
815                 Dimension indicatorSize = new Dimension(\r
816                         whiteKeySize.width / 2,\r
817                         whiteKeySize.height / 6\r
818                 );\r
819                 octaveRangeModel.setExtent( octaves );\r
820                 octaveRangeModel.setValue( (MAX_OCTAVES - octaves) / 2 );\r
821                 WIDTH_PER_OCTAVE = keyboard_size.width / octaves;\r
822                 //\r
823                 // Construct piano-keys\r
824                 //\r
825                 keys = new PianoKey[ octaves * 12 + 1 ];\r
826                 Vector<PianoKey> vBlackKeys = new Vector<PianoKey>();\r
827                 Vector<PianoKey> vWhiteKeys = new Vector<PianoKey>();\r
828                 Point keyPoint = new Point(1,1);\r
829                 PianoKey k;\r
830                 int i, i12;\r
831                 boolean is_CDE = true;\r
832                 for( i = i12 = 0; i < keys.length; i++, i12++ ) {\r
833                         switch(i12) {\r
834                         case 12: is_CDE = true; i12 = 0; break;\r
835                         case  5: is_CDE = false; break;\r
836                         default: break;\r
837                         }\r
838                         keyPoint.x = whiteKeySize.width * (\r
839                                 i / Music.SEMITONES_PER_OCTAVE * 7 + (i12+(is_CDE?1:2))/2\r
840                         );\r
841                         if( Music.isOnScale(i12,0) ) {\r
842                                 k = new PianoKey( keyPoint, whiteKeySize, indicatorSize );\r
843                                 k.isBlack = false;\r
844                                 vWhiteKeys.add(k);\r
845                         }\r
846                         else {\r
847                                 keyPoint.x -= ( (is_CDE?5:12) - i12 )/2 * blackKeySize.width / (is_CDE?3:4);\r
848                                 k = new PianoKey( keyPoint, blackKeySize, indicatorSize );\r
849                                 k.isBlack = true;\r
850                                 vBlackKeys.add(k);\r
851                         }\r
852                         (keys[i] = k).position = i;\r
853                 }\r
854                 whiteKeys = vWhiteKeys.toArray(new PianoKey[1]);\r
855                 blackKeys = vBlackKeys.toArray(new PianoKey[1]);\r
856                 changeKeyBinding(((octaves - 1) / 2) * 12, defaultBindedKeyChars);\r
857                 checkOutOfBounds();\r
858         }\r
859         //\r
860         void setDarkMode(boolean isDark) {\r
861                 this.isDark = isDark;\r
862                 setBackground( isDark ? Color.black : null );\r
863         }\r
864 }\r