OSDN Git Service

リファクタリング
[midichordhelper/MIDIChordHelper.git] / src / camidion / chordhelper / ChordHelperApplet.java
1 package camidion.chordhelper;
2 import java.awt.BorderLayout;
3 import java.awt.Color;
4 import java.awt.Dimension;
5 import java.awt.Image;
6 import java.awt.Insets;
7 import java.awt.event.ComponentAdapter;
8 import java.awt.event.ComponentEvent;
9 import java.awt.event.InputEvent;
10 import java.awt.event.MouseAdapter;
11 import java.awt.event.MouseEvent;
12 import java.io.IOException;
13 import java.net.URI;
14 import java.net.URISyntaxException;
15 import java.net.URL;
16 import java.security.AccessControlException;
17 import java.util.Arrays;
18
19 import javax.sound.midi.InvalidMidiDataException;
20 import javax.sound.midi.MetaMessage;
21 import javax.sound.midi.MidiSystem;
22 import javax.sound.midi.Sequence;
23 import javax.sound.midi.Sequencer;
24 import javax.swing.Box;
25 import javax.swing.BoxLayout;
26 import javax.swing.ImageIcon;
27 import javax.swing.JApplet;
28 import javax.swing.JButton;
29 import javax.swing.JComponent;
30 import javax.swing.JLabel;
31 import javax.swing.JLayeredPane;
32 import javax.swing.JPanel;
33 import javax.swing.JSlider;
34 import javax.swing.JSplitPane;
35 import javax.swing.JToggleButton;
36 import javax.swing.SwingUtilities;
37 import javax.swing.border.Border;
38
39 import camidion.chordhelper.anogakki.AnoGakkiPane;
40 import camidion.chordhelper.chorddiagram.CapoComboBoxModel;
41 import camidion.chordhelper.chorddiagram.ChordDiagram;
42 import camidion.chordhelper.chordmatrix.ChordButtonLabel;
43 import camidion.chordhelper.chordmatrix.ChordMatrix;
44 import camidion.chordhelper.chordmatrix.ChordMatrixListener;
45 import camidion.chordhelper.mididevice.MidiDeviceDialog;
46 import camidion.chordhelper.mididevice.MidiDeviceTreeModel;
47 import camidion.chordhelper.mididevice.MidiSequencerModel;
48 import camidion.chordhelper.mididevice.SequencerMeasureView;
49 import camidion.chordhelper.mididevice.SequencerTimeView;
50 import camidion.chordhelper.mididevice.VirtualMidiDevice;
51 import camidion.chordhelper.midieditor.Base64Dialog;
52 import camidion.chordhelper.midieditor.KeySignatureLabel;
53 import camidion.chordhelper.midieditor.MidiSequenceEditorDialog;
54 import camidion.chordhelper.midieditor.NewSequenceDialog;
55 import camidion.chordhelper.midieditor.PlaylistTableModel;
56 import camidion.chordhelper.midieditor.SequenceTickIndex;
57 import camidion.chordhelper.midieditor.SequenceTrackListTableModel;
58 import camidion.chordhelper.midieditor.TempoSelecter;
59 import camidion.chordhelper.midieditor.TimeSignatureSelecter;
60 import camidion.chordhelper.music.Chord;
61 import camidion.chordhelper.music.Key;
62 import camidion.chordhelper.music.Range;
63 import camidion.chordhelper.pianokeyboard.MidiKeyboardPanel;
64 import camidion.chordhelper.pianokeyboard.PianoKeyboardAdapter;
65
66 /**
67  * MIDI Chord Helper - Circle-of-fifth oriented chord pad
68  * (アプレットクラス)
69  *
70  *      @auther
71  *              Copyright (C) 2004-2017 @きよし - Akiyoshi Kamide
72  *              http://www.yk.rim.or.jp/~kamide/music/chordhelper/
73  */
74 public class ChordHelperApplet extends JApplet {
75         /////////////////////////////////////////////////////////////////////
76         //
77         // JavaScript などからの呼び出しインターフェース
78         //
79         /////////////////////////////////////////////////////////////////////
80         /**
81          * 未保存の修正済み MIDI ファイルがあるかどうか調べます。
82          * @return 未保存の修正済み MIDI ファイルがあれば true
83          */
84         public boolean isModified() {
85                 return playlistModel.getSequenceModelList().stream().anyMatch(m -> m.isModified());
86         }
87         /**
88          * 指定された小節数の曲を、乱数で自動作曲してプレイリストへ追加し、再生します。
89          * @param measureLength 小節数
90          * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1
91          * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
92          * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
93          */
94         public int addRandomSongToPlaylist(int measureLength) throws InvalidMidiDataException {
95                 NewSequenceDialog d = midiEditor.newSequenceDialog;
96                 d.setRandomChordProgression(measureLength);
97                 return playlistModel.play(d.getMidiSequence());
98         }
99         /**
100          * URLで指定されたMIDIファイルをプレイリストへ追加します。
101          *
102          * <p>URL の最後の / より後ろの部分がファイル名として取り込まれます。
103          * 指定できる MIDI ファイルには、param タグの midi_file パラメータと同様の制限があります。
104          * </p>
105          * @param midiFileUrl 追加するMIDIファイルのURL
106          * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1
107          */
108         public int addToPlaylist(String midiFileUrl) {
109                 try {
110                         URL url = (new URI(midiFileUrl)).toURL();
111                         String filename = url.getFile().replaceFirst("^.*/","");
112                         Sequence sequence = MidiSystem.getSequence(url);
113                         return playlistModel.add(sequence, filename);
114                 } catch( URISyntaxException|IOException|InvalidMidiDataException e ) {
115                         midiEditor.showWarning(e);
116                 } catch( AccessControlException e ) {
117                         midiEditor.showError(e);
118                 }
119                 return -1;
120         }
121         /**
122          * Base64 エンコードされた MIDI ファイルをプレイリストへ追加します。
123          *
124          * @param base64EncodedText Base64エンコードされたMIDIファイル
125          * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1
126          */
127         public int addToPlaylistBase64(String base64EncodedText) {
128                 return addToPlaylistBase64(base64EncodedText, null);
129         }
130         /**
131          * ファイル名を指定して、Base64エンコードされたMIDIファイルをプレイリストへ追加します。
132          *
133          * @param base64EncodedText Base64エンコードされたMIDIファイル
134          * @param filename ディレクトリ名を除いたファイル名
135          * @return 追加先のインデックス値(0から始まる)。追加できなかったときは -1
136          */
137         public int addToPlaylistBase64(String base64EncodedText, String filename) {
138                 Base64Dialog d = midiEditor.base64Dialog;
139                 d.setBase64Data(base64EncodedText, filename);
140                 return d.addToPlaylist();
141         }
142         /**
143          * プレイリスト上で現在選択されているMIDIシーケンスを、
144          * シーケンサへロードして再生します。
145          * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
146          * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
147          */
148         public void play() throws InvalidMidiDataException {
149                 play(playlistModel.sequenceListSelectionModel.getMinSelectionIndex());
150         }
151         /**
152          * 指定されたインデックス値が示すプレイリスト上のMIDIシーケンスをシーケンサへロードして再生します。
153          * @param index インデックス値(0から始まる)
154          * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
155          * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
156          */
157         public void play(int index) throws InvalidMidiDataException {
158                 playlistModel.play(index);
159         }
160         /**
161          * シーケンサが実行中かどうかを返します。
162          * {@link Sequencer#isRunning()} の戻り値をそのまま返します。
163          *
164          * @return 実行中のときtrue
165          */
166         public boolean isRunning() { return sequencerModel.getSequencer().isRunning(); }
167         /**
168          * シーケンサが再生中かどうかを返します。
169          * @return 再生中のときtrue
170          */
171         public boolean isPlaying() { return isRunning(); }
172         /**
173          * 現在シーケンサにロードされているMIDIデータをBase64テキストに変換した結果を返します。
174          * @return MIDIデータをBase64テキストに変換した結果(シーケンサにロードされていない場合null)
175          * @throws IOException MIDIデータの読み込みに失敗した場合
176          */
177         public String getMidiDataBase64() throws IOException {
178                 SequenceTrackListTableModel s = sequencerModel.getSequenceTrackListTableModel();
179                 if( s == null ) return null;
180                 Base64Dialog d = midiEditor.base64Dialog;
181                 d.setMIDIData(s.getMIDIdata());
182                 return d.getBase64Data();
183         }
184         /**
185          * 現在シーケンサにロードされているMIDIファイルのファイル名を返します。
186          * @return MIDIファイル名(設定されていないときは空文字列、シーケンサにロードされていない場合null)
187          */
188         public String getMidiFilename() {
189                 SequenceTrackListTableModel s = sequencerModel.getSequenceTrackListTableModel();
190                 if( s == null ) return null;
191                 String fn = s.getFilename();
192                 return fn == null ? "" : fn;
193         }
194         /**
195          * オクターブ位置を設定します。
196          * @param octavePosition オクターブ位置(デフォルト:4)
197          */
198         public void setOctavePosition(int octavePosition) {
199                 keyboardPanel.keyboardCenterPanel.keyboard.octaveRangeModel.setValue(octavePosition);
200         }
201         /**
202          * 操作対象のMIDIチャンネルを変更します。
203          * @param ch チャンネル番号 - 1(チャンネル1のとき0、デフォルトは0)
204          */
205         public void setChannel(int ch) {
206                 keyboardPanel.keyboardCenterPanel.keyboard.midiChComboboxModel.setSelectedChannel(ch);
207         }
208         /**
209          * 操作対象のMIDIチャンネルを返します。
210          * @return 操作対象のMIDIチャンネル
211          */
212         public int getChannel() {
213                 return keyboardPanel.keyboardCenterPanel.keyboard.midiChComboboxModel.getSelectedChannel();
214         }
215         /**
216          * 操作対象のMIDIチャンネルに対してプログラム(音色)を設定します。
217          * @param program 音色(0~127:General MIDI に基づく)
218          */
219         public void programChange(int program) {
220                 keyboardPanel.keyboardCenterPanel.keyboard.getSelectedChannel().programChange(program);
221         }
222         /**
223          * 操作対象のMIDIチャンネルに対してプログラム(音色)を設定します。
224          * 内部的には {@link #programChange(int)} を呼び出しているだけです。
225          * @param program 音色(0~127:General MIDI に基づく)
226          */
227         public void setProgram(int program) { programChange(program); }
228         /**
229          * 自動転回モードを変更します。初期値は true です。
230          * @param isAuto true:自動転回を行う false:自動転回を行わない
231          */
232         public void setAutoInversion(boolean isAuto) {
233                 inversionOmissionButton.setAutoInversion(isAuto);
234         }
235         /**
236          * 省略したい構成音を指定します。
237          * @param index
238          * <ul>
239          * <li>-1:省略しない(デフォルト)</li>
240          * <li>0:ルート音を省略</li>
241          * <li>1:三度を省略</li>
242          * <li>2:五度を省略</li>
243          * </ul>
244          */
245         public void setOmissionNoteIndex(int index) {
246                 inversionOmissionButton.setOmissionNoteIndex(index);
247         }
248         /**
249          * コードダイアグラムの表示・非表示を切り替えます。
250          * @param isVisible 表示するときtrue
251          */
252         public void setChordDiagramVisible(boolean isVisible) {
253                 keyboardSplitPane.resetToPreferredSizes();
254                 if( ! isVisible ) keyboardSplitPane.setDividerLocation((double)1.0);
255         }
256         /**
257          * コードダイヤグラムをギターモードに変更します。
258          * 初期状態ではウクレレモードになっています。
259          */
260         public void setChordDiagramForGuitar() {
261                 chordDiagram.setTargetInstrument(ChordDiagram.Instrument.Guitar);
262         }
263         /**
264          * ダークモード(暗い表示)と明るい表示とを切り替えます。
265          * @param isDark ダークモードのときtrue、明るい表示のときfalse(デフォルト)
266          */
267         public void setDarkMode(boolean isDark) {
268                 darkModeToggleButton.setSelected(isDark);
269         }
270         /**
271          * バージョン情報
272          */
273         public static class VersionInfo {
274                 public static final String NAME = "MIDI Chord Helper";
275                 public static final String VERSION = "Ver.20170409.1";
276                 public static final String COPYRIGHT = "Copyright (C) 2004-2017";
277                 public static final String AUTHER = "@きよし - Akiyoshi Kamide";
278                 public static final String URL = "http://www.yk.rim.or.jp/~kamide/music/chordhelper/";
279         }
280         @Override
281         public String getAppletInfo() {
282                 return String.join(" ",
283                                 VersionInfo.NAME, VersionInfo.VERSION,
284                                 VersionInfo.COPYRIGHT ,VersionInfo.AUTHER, VersionInfo.URL);
285         }
286         /**
287          * ボタンの余白を詰めたいときに setMargin() の引数に指定するインセット
288          */
289         public static final Insets ZERO_INSETS = new Insets(0,0,0,0);
290
291         // GUIコンポーネント(Javaアプリメインからの参照用)
292         MidiSequenceEditorDialog midiEditor;
293         PlaylistTableModel playlistModel;
294
295         // GUIコンポーネント(内部保存用)
296         private MidiSequencerModel sequencerModel;
297         private ChordMatrix chordMatrix;
298         private JPanel keyboardSequencerPanel;
299         private JPanel chordGuide;
300         private Color rootPaneDefaultBgcolor;
301         private Color lyricDisplayDefaultBgcolor;
302         private Border lyricDisplayDefaultBorder;
303         private JSplitPane mainSplitPane;
304         private JSplitPane keyboardSplitPane;
305         private ChordButtonLabel enterButtonLabel;
306         private ChordTextField  lyricDisplay;
307         private MidiKeyboardPanel keyboardPanel;
308         private InversionAndOmissionLabel inversionOmissionButton;
309         private JToggleButton darkModeToggleButton;
310         private ChordDiagram chordDiagram;
311         private KeySignatureLabel keysigLabel;
312         private AnoGakkiPane anoGakkiPane;
313         private JToggleButton anoGakkiToggleButton;
314         private MidiDeviceTreeModel deviceTreeModel;
315
316         // アイコン画像
317         private Image iconImage;
318         public Image getIconImage() { return iconImage; }
319         private ImageIcon imageIcon;
320         public ImageIcon getImageIcon() { return imageIcon; }
321
322         @Override
323         public void init() {
324                 // アイコン画像のロード
325                 URL imageIconUrl = getClass().getResource("midichordhelper.png");
326                 if( imageIconUrl != null ) {
327                         iconImage = (imageIcon = new ImageIcon(imageIconUrl)).getImage();
328                 }
329                 AboutMessagePane about = new AboutMessagePane(imageIcon);
330                 //
331                 // 背景色の取得
332                 rootPaneDefaultBgcolor = getContentPane().getBackground();
333                 //
334                 // コードダイアグラム、コードボタン、ピアノ鍵盤、およびそれらの仮想MIDIデバイスを生成
335                 CapoComboBoxModel capoComboBoxModel = new CapoComboBoxModel();
336                 chordDiagram = new ChordDiagram(capoComboBoxModel);
337                 chordMatrix = new ChordMatrix(capoComboBoxModel) {
338                         private void clearChord() {
339                                 chordOn();
340                                 keyboardPanel.keyboardCenterPanel.keyboard.chordDisplay.clear();
341                                 chordDiagram.clear();
342                         }
343                         {
344                                 addChordMatrixListener(new ChordMatrixListener(){
345                                         public void keySignatureChanged() {
346                                                 keyboardPanel.setCapoKey(getKeySignatureCapo());
347                                         }
348                                         public void chordChanged() { chordOn(); }
349                                 });
350                                 capoSelecter.checkbox.addItemListener(e->clearChord());
351                                 capoSelecter.valueSelecter.addActionListener(e->clearChord());
352                         }
353                 };
354                 keysigLabel = new KeySignatureLabel() {{
355                         addMouseListener(new MouseAdapter() {
356                                 @Override
357                                 public void mousePressed(MouseEvent e) {
358                                         chordMatrix.setKeySignature(getKey());
359                                 }
360                         });
361                 }};
362                 keyboardPanel = new MidiKeyboardPanel(chordMatrix) {{
363                         keyboardCenterPanel.keyboard.addPianoKeyboardListener(new PianoKeyboardAdapter() {
364                                 @Override
365                                 public void pianoKeyPressed(int n, InputEvent e) { chordDiagram.clear(); }
366                         });
367                         keySelecter.getKeysigCombobox().addActionListener(e->chordMatrix.setKeySignature(
368                                 keySelecter.getSelectedKey().transposedKey(-chordMatrix.capoSelecter.getCapo())
369                         ));
370                         keyboardCenterPanel.keyboard.setPreferredSize(new Dimension(571, 80));
371                 }};
372                 // MIDIデバイスとMIDIエディタのセットアップ
373                 VirtualMidiDevice guiMidiDevice = keyboardPanel.keyboardCenterPanel.keyboard.midiDevice;
374                 deviceTreeModel = new MidiDeviceTreeModel(guiMidiDevice);
375                 sequencerModel = deviceTreeModel.getSequencerModel();
376                 playlistModel = new PlaylistTableModel(sequencerModel);
377                 MidiDeviceDialog midiDeviceDialog = new MidiDeviceDialog(deviceTreeModel);
378                 midiDeviceDialog.setIconImage(iconImage);
379                 midiEditor = new MidiSequenceEditorDialog(playlistModel, guiMidiDevice, midiDeviceDialog.getOpenAction());
380                 midiEditor.setIconImage(iconImage);
381                 //
382                 // メイン画面へのMIDIファイルのドラッグ&ドロップ受付開始
383                 setTransferHandler(midiEditor.transferHandler);
384                 //
385                 // MIDIエディタのイベントダイアログを、ピアノ鍵盤のイベント送出ダイアログと共用
386                 keyboardPanel.setEventDialog(midiEditor.eventDialog);
387                 //
388                 // 歌詞表示/コード入力フィールド
389                 (lyricDisplay = new ChordTextField(sequencerModel)).addActionListener(
390                         e->chordMatrix.setSelectedChord(e.getActionCommand().trim().split("[ \t\r\n]")[0])
391                 );
392                 lyricDisplayDefaultBorder = lyricDisplay.getBorder();
393                 lyricDisplayDefaultBgcolor = lyricDisplay.getBackground();
394                 //
395                 // メタイベント(テンポ・拍子・調号)を受信して表示するリスナーを登録
396                 TempoSelecter tempoSelecter = new TempoSelecter() {{ setEditable(false); }};
397                 TimeSignatureSelecter timesigSelecter = new TimeSignatureSelecter() {{ setEditable(false); }};
398                 sequencerModel.getSequencer().addMetaEventListener(msg->{
399                         switch(msg.getType()) {
400                         case 0x51: // Tempo (3 bytes) - テンポ
401                                 SwingUtilities.invokeLater(()->tempoSelecter.setTempo(msg.getData()));
402                                 break;
403                         case 0x58: // Time signature (4 bytes) - 拍子
404                                 SwingUtilities.invokeLater(()->timesigSelecter.setValue(msg.getData()));
405                                 break;
406                         case 0x59: // Key signature (2 bytes) : 調号
407                                 SwingUtilities.invokeLater(()->setKeySignature(new Key(msg.getData())));
408                                 break;
409                         }
410                 });
411                 //シーケンサーの時間スライダーの値が変わったときのリスナーを登録
412                 JLabel songTitleLabel = new JLabel();
413                 sequencerModel.addChangeListener(e->{
414                         SequenceTrackListTableModel sequenceTrackListTableModel = sequencerModel.getSequenceTrackListTableModel();
415                         int loadedSequenceIndex = playlistModel.indexOfSequenceOnSequencer();
416                         songTitleLabel.setText("<html>"+(
417                                 loadedSequenceIndex < 0 ? "[No MIDI file loaded]" :
418                                 "MIDI file " + loadedSequenceIndex + ": " + (
419                                         sequenceTrackListTableModel == null ||
420                                         sequenceTrackListTableModel.toString().isEmpty() ?
421                                         "[Untitled]" :
422                                         "<font color=maroon>"+sequenceTrackListTableModel+"</font>"
423                                 )
424                         )+"</html>");
425                         Sequencer sequencer = sequencerModel.getSequencer();
426                         chordMatrix.setPlaying(sequencer.isRunning());
427                         if( sequenceTrackListTableModel != null ) {
428                                 SequenceTickIndex tickIndex = sequenceTrackListTableModel.getSequenceTickIndex();
429                                 long tickPos = sequencer.getTickPosition();
430                                 tickIndex.tickToMeasure(tickPos);
431                                 chordMatrix.setBeat(tickIndex);
432                                 if( sequencerModel.getValueIsAdjusting() || ! (sequencer.isRunning() || sequencer.isRecording()) ) {
433                                         MetaMessage msg;
434                                         msg = tickIndex.lastMetaMessageAt(
435                                                 SequenceTickIndex.MetaMessageType.TIME_SIGNATURE, tickPos
436                                         );
437                                         timesigSelecter.setValue(msg==null ? null : msg.getData());
438                                         msg = tickIndex.lastMetaMessageAt(
439                                                 SequenceTickIndex.MetaMessageType.TEMPO, tickPos
440                                         );
441                                         tempoSelecter.setTempo(msg==null ? null : msg.getData());
442                                         msg = tickIndex.lastMetaMessageAt(
443                                                 SequenceTickIndex.MetaMessageType.KEY_SIGNATURE, tickPos
444                                         );
445                                         if(msg == null) keysigLabel.clear(); else setKeySignature(new Key(msg.getData()));
446                                 }
447                         }
448                 });
449                 sequencerModel.fireStateChanged();
450                 chordGuide = new JPanel() {{
451                         setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
452                         add( Box.createHorizontalStrut(2) );
453                         add( chordMatrix.chordGuide );
454                         add( Box.createHorizontalStrut(2) );
455                         add( lyricDisplay );
456                         add( Box.createHorizontalStrut(2) );
457                         add( enterButtonLabel = new ChordButtonLabel("Enter",chordMatrix) {{
458                                 addMouseListener(new MouseAdapter() {
459                                         public void mousePressed(MouseEvent event) {
460                                                 boolean rightClicked = (event.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0;
461                                                 if( rightClicked )
462                                                         chordMatrix.setSelectedChord((Chord)null);
463                                                 else
464                                                         chordMatrix.setSelectedChord(lyricDisplay.getText());
465                                         }
466                                 });
467                         }});
468                         add( Box.createHorizontalStrut(5) );
469                         add( chordMatrix.chordDisplay );
470                         add( Box.createHorizontalStrut(5) );
471                         add( darkModeToggleButton = new JToggleButton(new ButtonIcon(ButtonIcon.DARK_MODE_ICON)) {{
472                                 setMargin(ZERO_INSETS);
473                                 addItemListener(e->innerSetDarkMode(darkModeToggleButton.isSelected()));
474                                 setToolTipText("Light / Dark - 明かりを点灯/消灯");
475                                 setBorder(null);
476                         }});
477                         add( Box.createHorizontalStrut(5) );
478                         add( anoGakkiToggleButton = new JToggleButton(new ButtonIcon(ButtonIcon.ANO_GAKKI_ICON)) {{
479                                 setOpaque(false);
480                                 setMargin(ZERO_INSETS);
481                                 setBorder(null);
482                                 setToolTipText("あの楽器");
483                                 addItemListener(e->
484                                         keyboardPanel.keyboardCenterPanel.keyboard.anoGakkiPane
485                                         = anoGakkiToggleButton.isSelected() ? anoGakkiPane : null
486                                 );
487                         }} );
488                         add( Box.createHorizontalStrut(5) );
489                         add( inversionOmissionButton = new InversionAndOmissionLabel() );
490                         add( Box.createHorizontalStrut(5) );
491                         add( chordMatrix.capoSelecter );
492                         add( Box.createHorizontalStrut(2) );
493                 }};
494                 keyboardSequencerPanel = new JPanel() {{
495                         setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
496                         add(chordGuide);
497                         add(Box.createVerticalStrut(5));
498                         add(keyboardSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, keyboardPanel, chordDiagram) {{
499                                 setOneTouchExpandable(true);
500                                 setResizeWeight(1.0);
501                                 setAlignmentX((float)0.5);
502                         }});
503                         add(Box.createVerticalStrut(5));
504                         add(new JPanel() {{
505                                 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
506                                 add(new JPanel() {{
507                                         setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
508                                         add(Box.createHorizontalStrut(12));
509                                         add(keysigLabel);
510                                         add(Box.createHorizontalStrut(12));
511                                         add(timesigSelecter);
512                                         add(Box.createHorizontalStrut(12));
513                                         add(tempoSelecter);
514                                         add(Box.createHorizontalStrut(12));
515                                         add(new SequencerMeasureView(sequencerModel));
516                                         add(Box.createHorizontalStrut(12));
517                                         add(songTitleLabel);
518                                         add(Box.createHorizontalStrut(12));
519                                         add(new JButton(midiEditor.openAction) {{ setMargin(ZERO_INSETS); }});
520                                 }});
521                                 add(new JPanel() {{
522                                         setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
523                                         add(Box.createHorizontalStrut(10));
524                                         add(new JSlider(sequencerModel));
525                                         add(new SequencerTimeView(sequencerModel));
526                                         add(Box.createHorizontalStrut(5));
527                                         add(new JButton(playlistModel.getMoveToTopAction()) {{ setMargin(ZERO_INSETS); }});
528                                         add(new JButton(sequencerModel.getMoveBackwardAction()) {{ setMargin(ZERO_INSETS); }});
529                                         add(new JToggleButton(sequencerModel.getStartStopAction()));
530                                         add(new JButton(sequencerModel.getMoveForwardAction()) {{ setMargin(ZERO_INSETS); }});
531                                         add(new JButton(playlistModel.getMoveToBottomAction()) {{ setMargin(ZERO_INSETS); }});
532                                         add(new JToggleButton(playlistModel.getToggleRepeatAction()) {{ setMargin(ZERO_INSETS); }});
533                                         add( Box.createHorizontalStrut(10) );
534                                 }});
535                                 add(new JPanel() {{
536                                         add(new JButton(midiDeviceDialog.getOpenAction()));
537                                         add(new JButton(about.getOpenAction()));
538                                 }});
539                         }});
540                 }};
541                 setContentPane(new JLayeredPane() {
542                         {
543                                 add(anoGakkiPane = new AnoGakkiPane(), JLayeredPane.PALETTE_LAYER);
544                                 addComponentListener(new ComponentAdapter() {
545                                         @Override
546                                         public void componentResized(ComponentEvent e) { adjustSize(); }
547                                         @Override
548                                         public void componentShown(ComponentEvent e) { adjustSize(); }
549                                         private void adjustSize() { anoGakkiPane.setBounds(getBounds()); }
550                                 });
551                                 setLayout(new BorderLayout());
552                                 setOpaque(true);
553                                 add(mainSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, chordMatrix, keyboardSequencerPanel){
554                                         {
555                                                 setResizeWeight(0.5);
556                                                 setAlignmentX((float)0.5);
557                                                 setDividerSize(5);
558                                         }
559                                 });
560                         }
561                 });
562                 setPreferredSize(new Dimension(750,470));
563         }
564         @Override
565         public void destroy() { deviceTreeModel.forEach(m -> m.close()); }
566         @Override
567         public void start() {
568                 //
569                 // コードボタンで設定されている現在の調をピアノキーボードに伝える
570                 chordMatrix.fireKeySignatureChanged();
571                 //
572                 // アプレットのパラメータにMIDIファイルのURLが指定されていたらそれを再生する
573                 String midiUrl = getParameter("midi_file");
574                 if( midiUrl != null ) try {
575                         play(addToPlaylist(midiUrl));
576                 } catch (Exception ex) {
577                         midiEditor.showWarning(ex);
578                 }
579         }
580         @Override
581         public void stop() { sequencerModel.stop(); }
582
583         private void innerSetDarkMode(boolean isDark) {
584                 Color col = isDark ? Color.black : null;
585                 getContentPane().setBackground(isDark ? Color.black : rootPaneDefaultBgcolor);
586                 mainSplitPane.setBackground(col);
587                 keyboardSplitPane.setBackground(col);
588                 enterButtonLabel.setDarkMode(isDark);
589                 chordGuide.setBackground(col);
590                 lyricDisplay.setBorder(isDark ? null : lyricDisplayDefaultBorder);
591                 lyricDisplay.setBackground(isDark ?
592                         chordMatrix.darkModeColorset.backgrounds[2] :
593                         lyricDisplayDefaultBgcolor
594                 );
595                 lyricDisplay.setForeground(isDark ? Color.white : null);
596                 inversionOmissionButton.setBackground(col);
597                 anoGakkiToggleButton.setBackground(col);
598                 keyboardSequencerPanel.setBackground(col);
599                 chordDiagram.setBackground(col);
600                 chordDiagram.titleLabel.setDarkMode(isDark);
601                 chordMatrix.setDarkMode(isDark);
602                 keyboardPanel.setDarkMode(isDark);
603         }
604
605         private void setKeySignature(Key key) {
606                 keysigLabel.setKey(key);
607                 chordMatrix.setKeySignature(key);
608         }
609
610         private int[] chordOnNotes = null;
611         /**
612          * 和音を発音します。
613          * <p>この関数を直接呼ぶとアルペジオが効かないので、
614          * chordMatrix.setSelectedChord() を使うことを推奨。
615          * </p>
616          */
617         public void chordOn() {
618                 Chord playChord = chordMatrix.getSelectedChord();
619                 if(
620                         chordOnNotes != null &&
621                         chordMatrix.getNoteIndex() < 0 &&
622                         (! chordMatrix.isDragged() || playChord == null)
623                 ) {
624                         // コードが鳴っている状態で、新たなコードを鳴らそうとしたり、
625                         // もう鳴らさないという信号が来た場合は、今鳴っている音を止める。
626                         Arrays.stream(chordOnNotes).forEach(n->keyboardPanel.keyboardCenterPanel.keyboard.noteOff(n));
627                         chordOnNotes = null;
628                 }
629                 if( playChord == null ) {
630                         // もう鳴らさないので、歌詞表示に通知して終了
631                         if( lyricDisplay != null ) lyricDisplay.appendChord(null);
632                         return;
633                 }
634                 // あの楽器っぽい表示
635                 if( keyboardPanel.keyboardCenterPanel.keyboard.anoGakkiPane != null ) {
636                         JComponent btn = chordMatrix.getSelectedButton();
637                         if( btn != null ) anoGakkiPane.start(chordMatrix, btn.getBounds());
638                 }
639                 // コードボタンからのコードを、カポつき演奏キーからオリジナルキーへ変換
640                 Key originalKey = chordMatrix.getKeySignatureCapo();
641                 Chord originalChord = playChord.transposedNewChord(
642                         chordMatrix.capoSelecter.getCapo(),
643                         chordMatrix.getKeySignature()
644                 );
645                 // 変換後のコードをキーボード画面に設定
646                 keyboardPanel.keyboardCenterPanel.keyboard.setChord(originalChord);
647                 //
648                 // 音域を決める。これにより鳴らす音が確定する。
649                 Range chordRange = new Range(
650                         keyboardPanel.keyboardCenterPanel.keyboard.getChromaticOffset() + 10 +
651                         ( keyboardPanel.keyboardCenterPanel.keyboard.getOctaves() / 4 ) * 12,
652                         inversionOmissionButton.isAutoInversionMode() ?
653                         keyboardPanel.keyboardCenterPanel.keyboard.getChromaticOffset() + 21 :
654                         keyboardPanel.keyboardCenterPanel.keyboard.getChromaticOffset() + 33,
655                         -2,
656                         inversionOmissionButton.isAutoInversionMode()
657                 );
658                 int[] notes = originalChord.toNoteArray(chordRange, originalKey);
659                 //
660                 // 前回鳴らしたコード構成音を覚えておく
661                 int[] prevChordOnNotes = null;
662                 if( chordMatrix.isDragged() || chordMatrix.getNoteIndex() >= 0 )
663                         prevChordOnNotes = Arrays.copyOf(chordOnNotes, chordOnNotes.length);
664                 //
665                 // 次に鳴らす構成音を決める
666                 chordOnNotes = new int[notes.length];
667                 int i = 0;
668                 for( int n : notes ) {
669                         if( inversionOmissionButton.getOmissionNoteIndex() == i ) {
670                                 i++; continue;
671                         }
672                         chordOnNotes[i++] = n;
673                         //
674                         // その音が今鳴っているか調べる
675                         boolean isNoteOn = prevChordOnNotes != null && Arrays.stream(prevChordOnNotes).anyMatch(prevN -> prevN == n);
676                         // すでに鳴っているのに単音を鳴らそうとする場合、
677                         // 鳴らそうとしている音を一旦止める。
678                         if( isNoteOn && chordMatrix.getNoteIndex() >= 0 &&
679                                 notes[chordMatrix.getNoteIndex()] - n == 0
680                         ) {
681                                 keyboardPanel.keyboardCenterPanel.keyboard.noteOff(n);
682                                 isNoteOn = false;
683                         }
684                         // その音が鳴っていなかったら鳴らす。
685                         if( ! isNoteOn ) keyboardPanel.keyboardCenterPanel.keyboard.noteOn(n);
686                 }
687                 //
688                 // コードを表示
689                 keyboardPanel.keyboardCenterPanel.keyboard.setChord(originalChord);
690                 chordMatrix.chordDisplay.setChord(playChord);
691                 //
692                 // コードダイアグラム用にもコードを表示
693                 Chord diagramChord;
694                 int chordDiagramCapo = chordDiagram.capoSelecterView.getCapo();
695                 if( chordDiagramCapo == chordMatrix.capoSelecter.getCapo() )
696                         diagramChord = playChord;
697                 else
698                         diagramChord = originalChord.transposedNewChord(-chordDiagramCapo, originalKey);
699                 chordDiagram.setChord(diagramChord);
700                 if( chordDiagram.recordTextButton.isSelected() )
701                         lyricDisplay.appendChord(diagramChord);
702         }
703
704 }
705