OSDN Git Service

リファクタリング(プレイリストビュークラスを外出し)
authorAkiyoshi Kamide <kamide@yk.rim.or.jp>
Mon, 17 Apr 2017 16:21:00 +0000 (01:21 +0900)
committerAkiyoshi Kamide <kamide@yk.rim.or.jp>
Mon, 17 Apr 2017 16:21:00 +0000 (01:21 +0900)
src/camidion/chordhelper/ChordHelperApplet.java
src/camidion/chordhelper/midieditor/MidiSequenceEditorDialog.java
src/camidion/chordhelper/midieditor/PlaylistTableModel.java
src/camidion/chordhelper/midieditor/SequenceListTable.java [new file with mode: 0644]
src/camidion/chordhelper/midieditor/SequenceTrackListTableModel.java

index 2bfaf68..1fe9068 100644 (file)
@@ -272,7 +272,7 @@ public class ChordHelperApplet extends JApplet {
         */
        public static class VersionInfo {
                public static final String NAME = "MIDI Chord Helper";
-               public static final String VERSION = "Ver.20170413.1";
+               public static final String VERSION = "Ver.20170417.1";
                public static final String COPYRIGHT = "Copyright (C) 2004-2017";
                public static final String AUTHER = "@きよし - Akiyoshi Kamide";
                public static final String URL = "http://www.yk.rim.or.jp/~kamide/music/chordhelper/";
@@ -410,7 +410,7 @@ public class ChordHelperApplet extends JApplet {
                JLabel songTitleLabel = new JLabel();
                sequencerModel.addChangeListener(e->{
                        SequenceTrackListTableModel sequenceModel = sequencerModel.getSequenceTrackListTableModel();
-                       int loadedSequenceIndex = playlistModel.indexOfSequenceOnSequencer();
+                       int loadedSequenceIndex = playlistModel.getSequenceModelList().indexOf(sequenceModel);
                        songTitleLabel.setText("<html>"+(
                                loadedSequenceIndex < 0 ? "[No MIDI file loaded]" :
                                "MIDI file " + loadedSequenceIndex + ": " + (
index 52db203..48a8388 100644 (file)
@@ -11,21 +11,13 @@ import java.awt.event.ComponentEvent;
 import java.awt.event.ComponentListener;
 import java.awt.event.MouseEvent;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.nio.charset.Charset;
-import java.security.AccessControlException;
 import java.util.Arrays;
 import java.util.EventObject;
-import java.util.Iterator;
 import java.util.List;
 
-import javax.sound.midi.InvalidMidiDataException;
 import javax.sound.midi.MidiChannel;
 import javax.sound.midi.MidiEvent;
 import javax.sound.midi.MidiMessage;
-import javax.sound.midi.MidiSystem;
 import javax.sound.midi.Sequencer;
 import javax.sound.midi.ShortMessage;
 import javax.swing.AbstractAction;
@@ -39,7 +31,6 @@ import javax.swing.JButton;
 import javax.swing.JCheckBox;
 import javax.swing.JComboBox;
 import javax.swing.JDialog;
-import javax.swing.JFileChooser;
 import javax.swing.JLabel;
 import javax.swing.JOptionPane;
 import javax.swing.JPanel;
@@ -53,11 +44,7 @@ import javax.swing.border.EtchedBorder;
 import javax.swing.event.ListSelectionEvent;
 import javax.swing.event.ListSelectionListener;
 import javax.swing.event.TableModelEvent;
-import javax.swing.filechooser.FileNameExtensionFilter;
-import javax.swing.table.JTableHeader;
 import javax.swing.table.TableCellEditor;
-import javax.swing.table.TableCellRenderer;
-import javax.swing.table.TableColumn;
 import javax.swing.table.TableColumnModel;
 import javax.swing.table.TableModel;
 
@@ -76,17 +63,21 @@ import camidion.chordhelper.music.MIDISpec;
  */
 public class MidiSequenceEditorDialog extends JDialog {
        /**
-        * このダイアログを表示するアクション
+        * このダイアログを開きます。表示されていなければ表示し、すでに表示されていたら最前面に移動します。
         */
-       public Action openAction = new AbstractAction("Edit/Playlist/Speed", new ButtonIcon(ButtonIcon.EDIT_ICON)) {
+       public void open() {
+               if( isVisible() ) toFront(); else setVisible(true);
+       }
+       /**
+        * このダイアログを表示するボタン用アクション
+        */
+       public final Action openAction = new AbstractAction("Edit/Playlist/Speed", new ButtonIcon(ButtonIcon.EDIT_ICON)) {
                {
                        String tooltip = "MIDIシーケンスの編集/プレイリスト/再生速度調整";
                        putValue(Action.SHORT_DESCRIPTION, tooltip);
                }
                @Override
-               public void actionPerformed(ActionEvent e) {
-                       if( isVisible() ) toFront(); else setVisible(true);
-               }
+               public void actionPerformed(ActionEvent e) { open(); }
        };
        /**
         * ドロップされた複数のMIDIファイルを読み込むハンドラー
@@ -100,12 +91,12 @@ public class MidiSequenceEditorDialog extends JDialog {
                @Override
                public boolean importData(TransferSupport support) {
                        try {
-                               play((List<File>)support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor));
+                               Object data = support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
+                               play((List<File>)data);
                                return true;
-                       } catch (Exception e) {
+                       } catch (Exception ex) {
                                JOptionPane.showMessageDialog(
-                                               MidiSequenceEditorDialog.this,
-                                               e,
+                                               MidiSequenceEditorDialog.this, ex,
                                                ChordHelperApplet.VersionInfo.NAME,
                                                JOptionPane.ERROR_MESSAGE);
                                return false;
@@ -125,45 +116,25 @@ public class MidiSequenceEditorDialog extends JDialog {
         * @param fileList 読み込むMIDIファイルのリスト
         */
        public void play(List<File> fileList) {
-               PlaylistTableModel playlist = getPlaylistModel();
-               int firstIndex = -1;
-               Iterator<File> itr = fileList.iterator();
-               while(itr.hasNext()) {
-                       File file = itr.next();
-                       try (FileInputStream in = new FileInputStream(file)) {
-                               int lastIndex = playlist.add(MidiSystem.getSequence(in), file.getName());
-                               if( firstIndex < 0 ) firstIndex = lastIndex;
-                       } catch(IOException|InvalidMidiDataException e) {
-                               String message = "Could not open as MIDI file "+file+"\n"+e;
-                               if( ! itr.hasNext() ) {
-                                       JOptionPane.showMessageDialog(this, message, ChordHelperApplet.VersionInfo.NAME, JOptionPane.WARNING_MESSAGE);
-                                       break;
-                               }
-                               if( JOptionPane.showConfirmDialog(this,
-                                               message + "\n\nContinue to open next file ?",
-                                               ChordHelperApplet.VersionInfo.NAME,
-                                               JOptionPane.YES_NO_OPTION,
-                                               JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION
-                               ) break;
-                       } catch(Exception e) {
-                               JOptionPane.showMessageDialog(this, e, ChordHelperApplet.VersionInfo.NAME, JOptionPane.ERROR_MESSAGE);
-                               break;
-                       }
-               }
+               play(sequenceListTable.add(fileList));
+       }
+       /**
+        * 指定されたインデックス値(先頭が0)のMIDIシーケンスから再生します。
+        * すでに再生されていた場合、このエディタダイアログを表示します。
+        * @param index プレイリスト内にあるMIDIシーケンスのインデックス値
+        */
+       public void play(int index) {
                try {
+                       PlaylistTableModel playlist = sequenceListTable.getModel();
                        MidiSequencerModel sequencerModel = playlist.getSequencerModel();
-                       if( sequencerModel.getSequencer().isRunning() ) {
-                               String command = (String)openAction.getValue(Action.NAME);
-                               openAction.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, command));
-                               return;
-                       }
-                       if( firstIndex >= 0 ) playlist.play(firstIndex);
+                       if( sequencerModel.getSequencer().isRunning() ) { open(); return; }
+                       if( index >= 0 ) playlist.play(index);
                } catch (Exception e) {
                        JOptionPane.showMessageDialog(this, e, ChordHelperApplet.VersionInfo.NAME, JOptionPane.ERROR_MESSAGE);
                }
        }
 
-       private static final Icon deleteIcon = new ButtonIcon(ButtonIcon.X_ICON);
+       static final Icon deleteIcon = new ButtonIcon(ButtonIcon.X_ICON);
        /**
         * 新しいMIDIシーケンスを生成するダイアログ
         */
@@ -189,347 +160,6 @@ public class MidiSequenceEditorDialog extends JDialog {
         */
        private VirtualMidiDevice outputMidiDevice;
        /**
-        * プレイリストビュー(シーケンスリスト)
-        */
-       public class SequenceListTable extends JTable {
-               /**
-                * ファイル選択ダイアログ(アプレットの場合は使用不可なのでnull)
-                */
-               private MidiFileChooser midiFileChooser;
-               /**
-                * BASE64エンコードアクション
-                */
-               private Action base64EncodeAction;
-               /**
-                * BASE64ダイアログ
-                */
-               public Base64Dialog base64Dialog;
-               /**
-                * MIDIデバイスダイアログを開くアクション
-                */
-               private Action midiDeviceDialogOpenAction;
-               /**
-                * プレイリストビューを構築します。
-                * @param model プレイリストデータモデル
-                */
-               public SequenceListTable(PlaylistTableModel model, Action midiDeviceDialogOpenAction) {
-                       super(model, null, model.sequenceListSelectionModel);
-                       this.midiDeviceDialogOpenAction = midiDeviceDialogOpenAction;
-                       try {
-                               midiFileChooser = new MidiFileChooser();
-                       }
-                       catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
-                               // アプレットの場合、Webクライアントマシンのローカルファイルには
-                               // アクセスできないので、ファイル選択ダイアログは使用不可。
-                               midiFileChooser = null;
-                       }
-                       // 再生ボタンを埋め込む
-                       new PlayButtonCellEditor();
-                       new PositionCellEditor();
-                       //
-                       // 文字コード選択をプルダウンにする
-                       int column = PlaylistTableModel.Column.CHARSET.ordinal();
-                       TableCellEditor ce = new DefaultCellEditor(new JComboBox<Charset>() {{
-                               Charset.availableCharsets().values().stream().forEach(v->addItem(v));
-                       }});
-                       getColumnModel().getColumn(column).setCellEditor(ce);
-                       setAutoCreateColumnsFromModel(false);
-                       //
-                       // Base64画面を開くアクションの生成
-                       base64Dialog = new Base64Dialog(model);
-                       base64EncodeAction = new AbstractAction("Base64") {
-                               {
-                                       String tooltip = "Base64 text conversion - Base64テキスト変換";
-                                       putValue(Action.SHORT_DESCRIPTION, tooltip);
-                               }
-                               @Override
-                               public void actionPerformed(ActionEvent e) {
-                                       SequenceTrackListTableModel mstm = getModel().getSelectedSequenceModel();
-                                       byte[] data = null;
-                                       String filename = null;
-                                       if( mstm != null ) {
-                                               filename = mstm.getFilename();
-                                               try {
-                                                       data = mstm.getMIDIdata();
-                                               } catch (IOException ioe) {
-                                                       base64Dialog.setText("File["+filename+"]:"+ioe.toString());
-                                                       base64Dialog.setVisible(true);
-                                                       return;
-                                               }
-                                       }
-                                       base64Dialog.setMIDIData(data, filename);
-                                       base64Dialog.setVisible(true);
-                               }
-                       };
-                       TableColumnModel colModel = getColumnModel();
-                       Arrays.stream(PlaylistTableModel.Column.values()).forEach(c->{
-                               TableColumn tc = colModel.getColumn(c.ordinal());
-                               tc.setPreferredWidth(c.preferredWidth);
-                               if( c == PlaylistTableModel.Column.LENGTH ) lengthColumn = tc;
-                       });
-               }
-               private TableColumn lengthColumn;
-               @Override
-               public void tableChanged(TableModelEvent event) {
-                       super.tableChanged(event);
-                       //
-                       // タイトルに合計シーケンス長を表示
-                       if( lengthColumn != null ) {
-                               int sec = getModel().getSecondLength();
-                               String title = PlaylistTableModel.Column.LENGTH.title;
-                               title = String.format(title+" [%02d:%02d]", sec/60, sec%60);
-                               lengthColumn.setHeaderValue(title);
-                       }
-                       // シーケンス削除時など、合計シーケンス長が変わっても
-                       // 列モデルからではヘッダタイトルが再描画されないことがある。
-                       // そこで、ヘッダビューから repaint() で突っついて再描画させる。
-                       JTableHeader th = getTableHeader();
-                       if( th != null ) th.repaint();
-               }
-               /** 時間位置を表示し、ダブルクリックによるシーケンサへのロードのみを受け付けるセルエディタ */
-               private class PositionCellEditor extends AbstractCellEditor implements TableCellEditor {
-                       public PositionCellEditor() {
-                               getColumnModel().getColumn(PlaylistTableModel.Column.POSITION.ordinal()).setCellEditor(this);
-                       }
-                       /**
-                        * セルをダブルクリックしたときだけ編集モードに入るようにします。
-                        * @param e イベント(マウスイベント)
-                        * @return 編集可能な場合true
-                        */
-                       @Override
-                       public boolean isCellEditable(EventObject e) {
-                               return (e instanceof MouseEvent) && ((MouseEvent)e).getClickCount() == 2;
-                       }
-                       @Override
-                       public Object getCellEditorValue() { return null; }
-                       /**
-                        * 編集モード時のコンポーネントを返すタイミングで
-                        * そのシーケンスをシーケンサーにロードしたあと、すぐに編集モードを解除します。
-                        * @return 常にnull
-                        */
-                       @Override
-                       public Component getTableCellEditorComponent(
-                               JTable table, Object value, boolean isSelected, int row, int column
-                       ) {
-                               try {
-                                       getModel().loadToSequencer(row);
-                               } catch (InvalidMidiDataException|IllegalStateException e) {
-                                       JOptionPane.showMessageDialog(
-                                                       SequenceListTable.this.getRootPane(),
-                                                       e,
-                                                       ChordHelperApplet.VersionInfo.NAME,
-                                                       JOptionPane.ERROR_MESSAGE);
-                               }
-                               fireEditingStopped();
-                               return null;
-                       }
-               }
-               /** 再生ボタンを埋め込んだセルの編集、描画を行うクラスです。 */
-               private class PlayButtonCellEditor extends AbstractCellEditor implements TableCellEditor, TableCellRenderer {
-                       /** 埋め込み用の再生ボタン */
-                       private JToggleButton playButton = new JToggleButton(getModel().getSequencerModel().getStartStopAction()) {
-                               { setMargin(ChordHelperApplet.ZERO_INSETS); }
-                       };
-                       /**
-                        * 埋め込み用のMIDIデバイス接続ボタン(そのシーケンスをロードしているシーケンサが開いていなかったときに表示)
-                        */
-                       private JButton midiDeviceConnectionButton = new JButton(midiDeviceDialogOpenAction) {
-                               { setMargin(ChordHelperApplet.ZERO_INSETS); }
-                       };
-                       /**
-                        * 再生ボタンを埋め込むセルエディタを構築し、列に対するレンダラ、エディタとして登録します。
-                        */
-                       public PlayButtonCellEditor() {
-                               TableColumn tc = getColumnModel().getColumn(PlaylistTableModel.Column.PLAY.ordinal());
-                               tc.setCellRenderer(this);
-                               tc.setCellEditor(this);
-                       }
-                       /**
-                        * {@inheritDoc}
-                        *
-                        * <p>この実装では、クリックしたセルのシーケンスがシーケンサーで再生可能な場合に
-                        * trueを返して再生ボタンを押せるようにします。
-                        * それ以外のセルについては、新たにシーケンサーへのロードを可能にするため、
-                        * ダブルクリックされたときだけtrueを返します。
-                        * </p>
-                        */
-                       @Override
-                       public boolean isCellEditable(EventObject e) {
-                               // マウスイベントのみを受け付け、それ以外はデフォルトエディタに振る
-                               if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
-                               //
-                               // エディタが編集を終了したことをリスナーに通知
-                               fireEditingStopped();
-                               //
-                               // クリックされたセルの行位置を把握(欄外だったら編集不可)
-                               MouseEvent me = (MouseEvent)e;
-                               int row = rowAtPoint(me.getPoint());
-                               if( row < 0 ) return false;
-                               //
-                               // シーケンサーにロード済みの場合は、シングルクリックを受け付ける。
-                               // それ以外は、ダブルクリックのみ受け付ける。
-                               return getModel().getSequenceModelList().get(row).isOnSequencer() || me.getClickCount() == 2;
-                       }
-                       @Override
-                       public Object getCellEditorValue() { return null; }
-                       /**
-                        * {@inheritDoc}
-                        *
-                        * <p>この実装では、行の表すシーケンスの状態に応じたボタンを表示します。
-                        * それ以外の場合は、新たにそのシーケンスをシーケンサーにロードしますが、
-                        * 以降の編集は不可としてnullを返します。
-                        * </p>
-                        */
-                       @Override
-                       public Component getTableCellEditorComponent(
-                               JTable table, Object value, boolean isSelected, int row, int column
-                       ) {
-                               fireEditingStopped();
-                               PlaylistTableModel model = getModel();
-                               if( model.getSequenceModelList().get(row).isOnSequencer() ) {
-                                       return model.getSequencerModel().getSequencer().isOpen() ? playButton : midiDeviceConnectionButton;
-                               }
-                               try {
-                                       model.loadToSequencer(row);
-                               } catch (InvalidMidiDataException e) {
-                                       JOptionPane.showMessageDialog(
-                                                       SequenceListTable.this.getRootPane(),
-                                                       e,
-                                                       ChordHelperApplet.VersionInfo.NAME,
-                                                       JOptionPane.ERROR_MESSAGE);
-                               }
-                               return null;
-                       }
-                       /**
-                        * {@inheritDoc}
-                        *
-                        * <p>この実装では、行の表すシーケンスの状態に応じたボタンを表示します。
-                        * それ以外の場合はデフォルトレンダラーに描画させます。
-                        * </p>
-                        */
-                       @Override
-                       public Component getTableCellRendererComponent(
-                               JTable table, Object value, boolean isSelected,
-                               boolean hasFocus, int row, int column
-                       ) {
-                               PlaylistTableModel model = getModel();
-                               if( model.getSequenceModelList().get(row).isOnSequencer() ) {
-                                       return model.getSequencerModel().getSequencer().isOpen() ? playButton : midiDeviceConnectionButton;
-                               }
-                               return table.getDefaultRenderer(model.getColumnClass(column))
-                                       .getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
-                       }
-               }
-               /**
-                * このプレイリスト(シーケンスリスト)が表示するデータを提供する
-                * プレイリストモデルを返します。
-                * @return プレイリストモデル
-                */
-               @Override
-               public PlaylistTableModel getModel() {
-                       return (PlaylistTableModel)super.getModel();
-               }
-               /**
-                * シーケンスを削除するアクション
-                */
-               Action deleteSequenceAction = getModel().new SelectedSequenceAction(
-                       "Delete", MidiSequenceEditorDialog.deleteIcon,
-                       "Delete selected MIDI sequence - 選択した曲をプレイリストから削除"
-               ) {
-                       private static final String CONFIRM_MESSAGE =
-                               "Selected MIDI sequence not saved - delete it from the playlist ?\n" +
-                               "選択したMIDIシーケンスはまだ保存されていません。プレイリストから削除しますか?";
-                       @Override
-                       public void actionPerformed(ActionEvent event) {
-                               PlaylistTableModel model = getModel();
-                               if( midiFileChooser != null ) {
-                                       SequenceTrackListTableModel seqModel = model.getSelectedSequenceModel();
-                                       if( seqModel != null && seqModel.isModified() && JOptionPane.showConfirmDialog(
-                                                       SequenceListTable.this.getRootPane(),
-                                                       CONFIRM_MESSAGE,
-                                                       ChordHelperApplet.VersionInfo.NAME,
-                                                       JOptionPane.YES_NO_OPTION,
-                                                       JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION
-                                       ) return;
-                               }
-                               try {
-                                       model.removeSelectedSequence();
-                               } catch (InvalidMidiDataException|IllegalStateException ex) {
-                                       JOptionPane.showMessageDialog(
-                                                       SequenceListTable.this.getRootPane(),
-                                                       ex,
-                                                       ChordHelperApplet.VersionInfo.NAME,
-                                                       JOptionPane.ERROR_MESSAGE);
-                               }
-                       }
-               };
-               /**
-                * ファイル選択ダイアログ(アプレットでは使用不可)
-                */
-               private class MidiFileChooser extends JFileChooser {
-                       { setFileFilter(new FileNameExtensionFilter("MIDI sequence (*.mid)", "mid")); }
-                       /**
-                        * ファイル保存アクション
-                        */
-                       public Action saveMidiFileAction = getModel().new SelectedSequenceAction(
-                               "Save",
-                               "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存"
-                       ) {
-                               @Override
-                               public void actionPerformed(ActionEvent event) {
-                                       SequenceTrackListTableModel sequenceModel = getModel().getSelectedSequenceModel();
-                                       if( sequenceModel == null ) return;
-                                       String fn = sequenceModel.getFilename();
-                                       if( fn != null && ! fn.isEmpty() ) setSelectedFile(new File(fn));
-                                       if( showSaveDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
-                                       File f = getSelectedFile();
-                                       if( f.exists() ) {
-                                               fn = f.getName();
-                                               if( JOptionPane.showConfirmDialog(
-                                                               SequenceListTable.this.getRootPane(),
-                                                               "Overwrite " + fn + " ?\n" + fn + " を上書きしてよろしいですか?",
-                                                               ChordHelperApplet.VersionInfo.NAME,
-                                                               JOptionPane.YES_NO_OPTION,
-                                                               JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION
-                                               ) return;
-                                       }
-                                       try ( FileOutputStream o = new FileOutputStream(f) ) {
-                                               o.write(sequenceModel.getMIDIdata());
-                                               sequenceModel.setModified(false);
-                                       }
-                                       catch( Exception ex ) {
-                                               JOptionPane.showMessageDialog(
-                                                               SequenceListTable.this.getRootPane(),
-                                                               ex,
-                                                               ChordHelperApplet.VersionInfo.NAME,
-                                                               JOptionPane.ERROR_MESSAGE);
-                                       }
-                               }
-                       };
-                       /**
-                        * ファイルを開くアクション
-                        */
-                       public Action openMidiFileAction = new AbstractAction("Open") {
-                               { putValue(Action.SHORT_DESCRIPTION, "Open MIDI file - MIDIファイルを開く"); }
-                               @Override
-                               public void actionPerformed(ActionEvent event) {
-                                       try {
-                                               if( showOpenDialog((Component)event.getSource()) == JFileChooser.APPROVE_OPTION ) {
-                                                       play(Arrays.asList(getSelectedFile()));
-                                               }
-                                       } catch( Exception ex ) {
-                                               JOptionPane.showMessageDialog(
-                                                               SequenceListTable.this.getRootPane(),
-                                                               ex,
-                                                               ChordHelperApplet.VersionInfo.NAME,
-                                                               JOptionPane.ERROR_MESSAGE);
-                                       }
-                               }
-                       };
-               };
-       }
-
-       /**
         * シーケンス(トラックリスト)テーブルビュー
         */
        public class TrackListTable extends JTable {
index 4972ff7..18244b6 100644 (file)
@@ -48,7 +48,7 @@ public class PlaylistTableModel extends AbstractTableModel {
        /**
         * 選択されているシーケンスのインデックス
         */
-       public ListSelectionModel sequenceListSelectionModel = new DefaultListSelectionModel() {
+       public final ListSelectionModel sequenceListSelectionModel = new DefaultListSelectionModel() {
                {
                        setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
                }
@@ -69,10 +69,12 @@ public class PlaylistTableModel extends AbstractTableModel {
                public void stateChanged(ChangeEvent event) {
                        Object src = event.getSource();
                        if( src instanceof MidiSequencerModel ) {
-                               int newValue = ((MidiSequencerModel)src).getValue() / 1000;
+                               MidiSequencerModel sequencerModel = (MidiSequencerModel)src;
+                               int newValue = sequencerModel.getValue() / 1000;
                                if(value != newValue) {
                                        value = newValue;
-                                       fireTableCellUpdated(indexOfSequenceOnSequencer(), Column.POSITION.ordinal());
+                                       int rowIndex = sequenceModelList.indexOf(sequencerModel.getSequenceTrackListTableModel());
+                                       fireTableCellUpdated(rowIndex, Column.POSITION.ordinal());
                                }
                        }
                }
@@ -120,7 +122,8 @@ public class PlaylistTableModel extends AbstractTableModel {
                        sequencerModel.stop();
                        // ここでボタンが停止状態に変わったはずなので、通常であれば再生ボタンが自力で再描画するところだが、
                        // セルのレンダラーが描く再生ボタンには効かないようなので、セルを突っついて再表示させる。
-                       fireTableCellUpdated(indexOfSequenceOnSequencer(), Column.PLAY.ordinal());
+                       int rowIndex = sequenceModelList.indexOf(sequencerModel.getSequenceTrackListTableModel());
+                       fireTableCellUpdated(rowIndex, Column.PLAY.ordinal());
                }
        }
        /**
@@ -400,22 +403,18 @@ public class PlaylistTableModel extends AbstractTableModel {
                sequenceListSelectionModel.setSelectionInterval(lastIndex, lastIndex);
                return lastIndex;
        }
-
        /**
-        * 選択されたシーケンスを除去します。
-        * 除去されたシーケンスがシーケンサーにロード済みの場合、アンロードします。
-        *
+        * MIDIシーケンスを除去します。除去されたMIDIシーケンスがシーケンサーにロード済みだった場合、アンロードします。
+        * @param rowIndex 除去するMIDIシーケンスのインデックス(先頭が 0)
+        * @return 除去されたMIDIシーケンス
         * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
         * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
         */
-       public void removeSelectedSequence() throws InvalidMidiDataException {
-               if( sequenceListSelectionModel.isSelectionEmpty() ) return;
-               int selectedIndex = sequenceListSelectionModel.getMinSelectionIndex();
-               SequenceTrackListTableModel removedSequence = sequenceModelList.remove(selectedIndex);
-               fireTableRowsDeleted(selectedIndex, selectedIndex);
-               if( removedSequence.isOnSequencer() ) {
-                       sequencerModel.setSequenceTrackListTableModel(null);
-               }
+       public SequenceTrackListTableModel remove(int rowIndex) throws InvalidMidiDataException {
+               SequenceTrackListTableModel removedSequence = sequenceModelList.remove(rowIndex);
+               fireTableRowsDeleted(rowIndex, rowIndex);
+               if(removedSequence.isOnSequencer()) sequencerModel.setSequenceTrackListTableModel(null);
+               return removedSequence;
        }
        /**
         * テーブル内の指定したインデックス位置にあるシーケンスをシーケンサーにロードします。
@@ -466,21 +465,13 @@ public class PlaylistTableModel extends AbstractTableModel {
                return lastIndex;
        }
        /**
-        * 現在シーケンサにロードされているシーケンスのインデックスを返します。
-        * ロードされていない場合は -1 を返します。
-        * @return 現在シーケンサにロードされているシーケンスのインデックス
-        */
-       public int indexOfSequenceOnSequencer() {
-               return sequenceModelList.indexOf(sequencerModel.getSequenceTrackListTableModel());
-       }
-       /**
         * 引数で示された数だけ次へ進めたシーケンスをロードします。
         * @param offset 進みたいシーケンス数
         * @return 以前と異なるインデックスのシーケンスをロードできた場合true
         * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
         */
        private boolean loadNext(int offset) throws InvalidMidiDataException {
-               int loadedIndex = indexOfSequenceOnSequencer();
+               int loadedIndex = sequenceModelList.indexOf(sequencerModel.getSequenceTrackListTableModel());
                int newIndex = loadedIndex + offset;
                if( newIndex < 0 ) newIndex = 0; else {
                        int sz = sequenceModelList.size();
diff --git a/src/camidion/chordhelper/midieditor/SequenceListTable.java b/src/camidion/chordhelper/midieditor/SequenceListTable.java
new file mode 100644 (file)
index 0000000..9e378db
--- /dev/null
@@ -0,0 +1,434 @@
+package camidion.chordhelper.midieditor;
+
+import java.awt.Component;
+import java.awt.HeadlessException;
+import java.awt.event.ActionEvent;
+import java.awt.event.MouseEvent;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.security.AccessControlException;
+import java.util.Arrays;
+import java.util.EventObject;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.sound.midi.InvalidMidiDataException;
+import javax.sound.midi.MidiSystem;
+import javax.swing.AbstractAction;
+import javax.swing.AbstractCellEditor;
+import javax.swing.Action;
+import javax.swing.DefaultCellEditor;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JFileChooser;
+import javax.swing.JOptionPane;
+import javax.swing.JRootPane;
+import javax.swing.JTable;
+import javax.swing.JToggleButton;
+import javax.swing.event.TableModelEvent;
+import javax.swing.filechooser.FileNameExtensionFilter;
+import javax.swing.table.JTableHeader;
+import javax.swing.table.TableCellEditor;
+import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableColumn;
+import javax.swing.table.TableColumnModel;
+
+import camidion.chordhelper.ChordHelperApplet;
+import camidion.chordhelper.mididevice.MidiSequencerModel;
+
+/**
+ * プレイリストビュー(シーケンスリスト)
+ */
+public class SequenceListTable extends JTable {
+       /**
+        * ファイル選択ダイアログ(アプレットの場合は使用不可なのでnull)
+        */
+       MidiFileChooser midiFileChooser;
+       /**
+        * BASE64エンコードアクション
+        */
+       Action base64EncodeAction;
+       /**
+        * BASE64ダイアログ
+        */
+       public Base64Dialog base64Dialog;
+       /**
+        * MIDIデバイスダイアログを開くアクション
+        */
+       private Action midiDeviceDialogOpenAction;
+       /**
+        * プレイリストビューを構築します。
+        * @param model プレイリストデータモデル
+        */
+       public SequenceListTable(PlaylistTableModel model, Action midiDeviceDialogOpenAction) {
+               super(model, null, model.sequenceListSelectionModel);
+               this.midiDeviceDialogOpenAction = midiDeviceDialogOpenAction;
+               try {
+                       midiFileChooser = new MidiFileChooser();
+               }
+               catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
+                       // アプレットの場合、Webクライアントマシンのローカルファイルには
+                       // アクセスできないので、ファイル選択ダイアログは使用不可。
+                       midiFileChooser = null;
+               }
+               // 再生ボタンを埋め込む
+               new PlayButtonCellEditor();
+               new PositionCellEditor();
+               //
+               // 文字コード選択をプルダウンにする
+               int column = PlaylistTableModel.Column.CHARSET.ordinal();
+               TableCellEditor ce = new DefaultCellEditor(new JComboBox<Charset>() {{
+                       Charset.availableCharsets().values().stream().forEach(v->addItem(v));
+               }});
+               getColumnModel().getColumn(column).setCellEditor(ce);
+               setAutoCreateColumnsFromModel(false);
+               //
+               // Base64画面を開くアクションの生成
+               base64Dialog = new Base64Dialog(model);
+               base64EncodeAction = new AbstractAction("Base64") {
+                       {
+                               String tooltip = "Base64 text conversion - Base64テキスト変換";
+                               putValue(Action.SHORT_DESCRIPTION, tooltip);
+                       }
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               SequenceTrackListTableModel mstm = getModel().getSelectedSequenceModel();
+                               byte[] data = null;
+                               String filename = null;
+                               if( mstm != null ) {
+                                       filename = mstm.getFilename();
+                                       try {
+                                               data = mstm.getMIDIdata();
+                                       } catch (IOException ioe) {
+                                               base64Dialog.setText("File["+filename+"]:"+ioe.toString());
+                                               base64Dialog.setVisible(true);
+                                               return;
+                                       }
+                               }
+                               base64Dialog.setMIDIData(data, filename);
+                               base64Dialog.setVisible(true);
+                       }
+               };
+               TableColumnModel colModel = getColumnModel();
+               Arrays.stream(PlaylistTableModel.Column.values()).forEach(c->{
+                       TableColumn tc = colModel.getColumn(c.ordinal());
+                       tc.setPreferredWidth(c.preferredWidth);
+                       if( c == PlaylistTableModel.Column.LENGTH ) lengthColumn = tc;
+               });
+       }
+       private TableColumn lengthColumn;
+       @Override
+       public void tableChanged(TableModelEvent event) {
+               super.tableChanged(event);
+               //
+               // タイトルに合計シーケンス長を表示
+               if( lengthColumn != null ) {
+                       int sec = getModel().getSecondLength();
+                       String title = PlaylistTableModel.Column.LENGTH.title;
+                       title = String.format(title+" [%02d:%02d]", sec/60, sec%60);
+                       lengthColumn.setHeaderValue(title);
+               }
+               // シーケンス削除時など、合計シーケンス長が変わっても
+               // 列モデルからではヘッダタイトルが再描画されないことがある。
+               // そこで、ヘッダビューから repaint() で突っついて再描画させる。
+               JTableHeader th = getTableHeader();
+               if( th != null ) th.repaint();
+       }
+       /** 時間位置を表示し、ダブルクリックによるシーケンサへのロードのみを受け付けるセルエディタ */
+       private class PositionCellEditor extends AbstractCellEditor implements TableCellEditor {
+               public PositionCellEditor() {
+                       getColumnModel().getColumn(PlaylistTableModel.Column.POSITION.ordinal()).setCellEditor(this);
+               }
+               /**
+                * セルをダブルクリックしたときだけ編集モードに入るようにします。
+                * @param e イベント(マウスイベント)
+                * @return 編集可能な場合true
+                */
+               @Override
+               public boolean isCellEditable(EventObject e) {
+                       return (e instanceof MouseEvent) && ((MouseEvent)e).getClickCount() == 2;
+               }
+               @Override
+               public Object getCellEditorValue() { return null; }
+               /**
+                * 編集モード時のコンポーネントを返すタイミングで
+                * そのシーケンスをシーケンサーにロードしたあと、すぐに編集モードを解除します。
+                * @return 常にnull
+                */
+               @Override
+               public Component getTableCellEditorComponent(
+                       JTable table, Object value, boolean isSelected, int row, int column
+               ) {
+                       try {
+                               getModel().loadToSequencer(row);
+                       } catch (InvalidMidiDataException|IllegalStateException e) {
+                               JOptionPane.showMessageDialog(
+                                               SequenceListTable.this.getRootPane(),
+                                               e,
+                                               ChordHelperApplet.VersionInfo.NAME,
+                                               JOptionPane.ERROR_MESSAGE);
+                       }
+                       fireEditingStopped();
+                       return null;
+               }
+       }
+       /** 再生ボタンを埋め込んだセルの編集、描画を行うクラスです。 */
+       private class PlayButtonCellEditor extends AbstractCellEditor implements TableCellEditor, TableCellRenderer {
+               /** 埋め込み用の再生ボタン */
+               private JToggleButton playButton = new JToggleButton(getModel().getSequencerModel().getStartStopAction()) {
+                       { setMargin(ChordHelperApplet.ZERO_INSETS); }
+               };
+               /**
+                * 埋め込み用のMIDIデバイス接続ボタン(そのシーケンスをロードしているシーケンサが開いていなかったときに表示)
+                */
+               private JButton midiDeviceConnectionButton = new JButton(midiDeviceDialogOpenAction) {
+                       { setMargin(ChordHelperApplet.ZERO_INSETS); }
+               };
+               /**
+                * 再生ボタンを埋め込むセルエディタを構築し、列に対するレンダラ、エディタとして登録します。
+                */
+               public PlayButtonCellEditor() {
+                       TableColumn tc = getColumnModel().getColumn(PlaylistTableModel.Column.PLAY.ordinal());
+                       tc.setCellRenderer(this);
+                       tc.setCellEditor(this);
+               }
+               /**
+                * {@inheritDoc}
+                *
+                * <p>この実装では、クリックしたセルのシーケンスがシーケンサーで再生可能な場合に
+                * trueを返して再生ボタンを押せるようにします。
+                * それ以外のセルについては、新たにシーケンサーへのロードを可能にするため、
+                * ダブルクリックされたときだけtrueを返します。
+                * </p>
+                */
+               @Override
+               public boolean isCellEditable(EventObject e) {
+                       // マウスイベントのみを受け付け、それ以外はデフォルトエディタに振る
+                       if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
+                       //
+                       // エディタが編集を終了したことをリスナーに通知
+                       fireEditingStopped();
+                       //
+                       // クリックされたセルの行位置を把握(欄外だったら編集不可)
+                       MouseEvent me = (MouseEvent)e;
+                       int row = rowAtPoint(me.getPoint());
+                       if( row < 0 ) return false;
+                       //
+                       // シーケンサーにロード済みの場合は、シングルクリックを受け付ける。
+                       // それ以外は、ダブルクリックのみ受け付ける。
+                       return getModel().getSequenceModelList().get(row).isOnSequencer() || me.getClickCount() == 2;
+               }
+               @Override
+               public Object getCellEditorValue() { return null; }
+               /**
+                * {@inheritDoc}
+                *
+                * <p>この実装では、行の表すシーケンスの状態に応じたボタンを表示します。
+                * それ以外の場合は、新たにそのシーケンスをシーケンサーにロードしますが、
+                * 以降の編集は不可としてnullを返します。
+                * </p>
+                */
+               @Override
+               public Component getTableCellEditorComponent(
+                       JTable table, Object value, boolean isSelected, int row, int column
+               ) {
+                       fireEditingStopped();
+                       PlaylistTableModel model = getModel();
+                       if( model.getSequenceModelList().get(row).isOnSequencer() ) {
+                               return model.getSequencerModel().getSequencer().isOpen() ? playButton : midiDeviceConnectionButton;
+                       }
+                       try {
+                               model.loadToSequencer(row);
+                       } catch (InvalidMidiDataException e) {
+                               JOptionPane.showMessageDialog(
+                                               SequenceListTable.this.getRootPane(),
+                                               e,
+                                               ChordHelperApplet.VersionInfo.NAME,
+                                               JOptionPane.ERROR_MESSAGE);
+                       }
+                       return null;
+               }
+               /**
+                * {@inheritDoc}
+                *
+                * <p>この実装では、行の表すシーケンスの状態に応じたボタンを表示します。
+                * それ以外の場合はデフォルトレンダラーに描画させます。
+                * </p>
+                */
+               @Override
+               public Component getTableCellRendererComponent(
+                       JTable table, Object value, boolean isSelected,
+                       boolean hasFocus, int row, int column
+               ) {
+                       PlaylistTableModel model = getModel();
+                       if( model.getSequenceModelList().get(row).isOnSequencer() ) {
+                               return model.getSequencerModel().getSequencer().isOpen() ? playButton : midiDeviceConnectionButton;
+                       }
+                       return table.getDefaultRenderer(model.getColumnClass(column))
+                               .getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
+               }
+       }
+       /**
+        * このプレイリスト(シーケンスリスト)が表示するデータを提供する
+        * プレイリストモデルを返します。
+        * @return プレイリストモデル
+        */
+       @Override
+       public PlaylistTableModel getModel() {
+               return (PlaylistTableModel)super.getModel();
+       }
+       /**
+        * このプレイリストにMIDIファイルを追加します。追加に失敗した場合はダイアログを表示し、
+        * 後続のMIDIファイルが残っていればそれを追加するかどうかをユーザに尋ねます。
+        * @param fileList MIDIファイルのリスト
+        * @return 追加されたMIDIファイルのインデックス値(先頭が0、追加されなかった場合は-1)
+        */
+       public int add(List<File> fileList) {
+               PlaylistTableModel model = getModel();
+               int firstIndex = -1;
+               Iterator<File> itr = fileList.iterator();
+               while(itr.hasNext()) {
+                       File file = itr.next();
+                       try (FileInputStream in = new FileInputStream(file)) {
+                               int lastIndex = model.add(MidiSystem.getSequence(in), file.getName());
+                               if( firstIndex < 0 ) firstIndex = lastIndex;
+                       } catch(IOException|InvalidMidiDataException e) {
+                               String message = "Could not open as MIDI file "+file+"\n"+e;
+                               if( ! itr.hasNext() ) {
+                                       JOptionPane.showMessageDialog(
+                                                       getRootPane(), message,
+                                                       ChordHelperApplet.VersionInfo.NAME,
+                                                       JOptionPane.WARNING_MESSAGE);
+                                       break;
+                               }
+                               if( JOptionPane.showConfirmDialog(
+                                               getRootPane(),
+                                               message + "\n\nContinue to open next file ?",
+                                               ChordHelperApplet.VersionInfo.NAME,
+                                               JOptionPane.YES_NO_OPTION,
+                                               JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION
+                               ) break;
+                       } catch(Exception ex) {
+                               JOptionPane.showMessageDialog(
+                                               getRootPane(), ex,
+                                               ChordHelperApplet.VersionInfo.NAME,
+                                               JOptionPane.ERROR_MESSAGE);
+                               break;
+                       }
+               }
+               return firstIndex;
+       }
+       /**
+        * シーケンスを削除するアクション
+        */
+       Action deleteSequenceAction = getModel().new SelectedSequenceAction(
+               "Delete", MidiSequenceEditorDialog.deleteIcon,
+               "Delete selected MIDI sequence - 選択した曲をプレイリストから削除"
+       ) {
+               private static final String CONFIRM_MESSAGE =
+                       "Selected MIDI sequence not saved - delete it from the playlist ?\n" +
+                       "選択したMIDIシーケンスはまだ保存されていません。プレイリストから削除しますか?";
+               @Override
+               public void actionPerformed(ActionEvent event) {
+                       PlaylistTableModel model = getModel();
+                       if( midiFileChooser != null ) {
+                               SequenceTrackListTableModel seqModel = model.getSelectedSequenceModel();
+                               if( seqModel != null && seqModel.isModified() && JOptionPane.showConfirmDialog(
+                                               SequenceListTable.this.getRootPane(),
+                                               CONFIRM_MESSAGE,
+                                               ChordHelperApplet.VersionInfo.NAME,
+                                               JOptionPane.YES_NO_OPTION,
+                                               JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION
+                               ) return;
+                       }
+                       if( model.sequenceListSelectionModel.isSelectionEmpty() ) return;
+                       int index = model.sequenceListSelectionModel.getMinSelectionIndex();
+                       try {
+                               model.remove(index);
+                       } catch (Exception ex) {
+                               JOptionPane.showMessageDialog(
+                                               SequenceListTable.this.getRootPane(),
+                                               ex,
+                                               ChordHelperApplet.VersionInfo.NAME,
+                                               JOptionPane.ERROR_MESSAGE);
+                       }
+               }
+       };
+       /**
+        * ファイル選択ダイアログ(アプレットでは使用不可)
+        */
+       class MidiFileChooser extends JFileChooser {
+               { setFileFilter(new FileNameExtensionFilter("MIDI sequence (*.mid)", "mid")); }
+               /**
+                * ファイル保存アクション
+                */
+               public Action saveMidiFileAction = getModel().new SelectedSequenceAction(
+                       "Save",
+                       "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存"
+               ) {
+                       @Override
+                       public void actionPerformed(ActionEvent event) {
+                               SequenceListTable playlistView = SequenceListTable.this;
+                               SequenceTrackListTableModel sequenceModel = playlistView.getModel().getSelectedSequenceModel();
+                               if( sequenceModel == null ) return;
+                               String fn = sequenceModel.getFilename();
+                               if( fn != null && ! fn.isEmpty() ) setSelectedFile(new File(fn));
+                               if( showSaveDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
+                               File f = getSelectedFile();
+                               if( f.exists() ) {
+                                       fn = f.getName();
+                                       if( JOptionPane.showConfirmDialog(
+                                                       playlistView.getRootPane(),
+                                                       "Overwrite " + fn + " ?\n" + fn + " を上書きしてよろしいですか?",
+                                                       ChordHelperApplet.VersionInfo.NAME,
+                                                       JOptionPane.YES_NO_OPTION,
+                                                       JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION
+                                       ) return;
+                               }
+                               try ( FileOutputStream o = new FileOutputStream(f) ) {
+                                       o.write(sequenceModel.getMIDIdata());
+                                       sequenceModel.setModified(false);
+                               }
+                               catch( Exception ex ) {
+                                       JOptionPane.showMessageDialog(
+                                                       playlistView.getRootPane(),
+                                                       ex,
+                                                       ChordHelperApplet.VersionInfo.NAME,
+                                                       JOptionPane.ERROR_MESSAGE);
+                               }
+                       }
+               };
+               /**
+                * ファイルを開くアクション
+                */
+               public Action openMidiFileAction = new AbstractAction("Open") {
+                       { putValue(Action.SHORT_DESCRIPTION, "Open MIDI file - MIDIファイルを開く"); }
+                       @Override
+                       public void actionPerformed(ActionEvent event) {
+                               SequenceListTable playlistView = SequenceListTable.this;
+                               JRootPane rootPane = playlistView.getRootPane();
+                               try {
+                                       if( showOpenDialog(rootPane) != JFileChooser.APPROVE_OPTION ) return;
+                               } catch( HeadlessException ex ) {
+                                       ex.printStackTrace();
+                                       return;
+                               }
+                               int firstIndex = playlistView.add(Arrays.asList(getSelectedFile()));
+                               try {
+                                       PlaylistTableModel playlist = playlistView.getModel();
+                                       MidiSequencerModel sequencerModel = playlist.getSequencerModel();
+                                       if( sequencerModel.getSequencer().isRunning() ) return;
+                                       if( firstIndex >= 0 ) playlist.play(firstIndex);
+                               } catch (Exception ex) {
+                                       JOptionPane.showMessageDialog(
+                                                       rootPane, ex,
+                                                       ChordHelperApplet.VersionInfo.NAME,
+                                                       JOptionPane.ERROR_MESSAGE);
+                               }
+                       }
+               };
+       };
+}
\ No newline at end of file
index 291fe4a..1c9f044 100644 (file)
@@ -374,8 +374,7 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
                int maxIndex = trackListSelectionModel.getMaxSelectionIndex();
                Track tracks[] = sequence.getTracks();
                for( int i = maxIndex; i >= minIndex; i-- ) {
-                       if( ! trackListSelectionModel.isSelectedIndex(i) )
-                               continue;
+                       if( ! trackListSelectionModel.isSelectedIndex(i) ) continue;
                        sequence.deleteTrack(tracks[i]);
                        trackModelList.remove(i);
                }