]> ruin.nu Git - moosique.git/blob - MooTrackView.java
no message
[moosique.git] / MooTrackView.java
1 import javax.swing.*;
2 import java.awt.*;
3 import java.awt.event.*;
4 import javax.sound.midi.*;
5 import java.util.*;
6
7 /**
8  * Graphical representation of a MIDI track.
9  * 
10  * @author  Andersson , Andreen, Lanneskog, Pehrson
11  * @version 1
12  */
13
14 public class MooTrackView extends JPanel {
15
16         private Track track;
17         private MooTrackTitle title;
18         private MooKeyboard keyboard;
19
20         private JPopupMenu popup, selPopup;
21         private JMenu selPopupTranspUp, selPopupTranspDown;
22         private JMenuItem popupAdd, popupPaste;
23         private JMenuItem selPopupCopy, selPopupCut, selPopupRemove;
24         private JMenuItem[] selPopupTranspUpItems, selPopupTranspDownItems;
25
26         private ArrayList coords; 
27         private TreeSet selection;
28         private Insets insets;
29         private Rectangle box;
30         private int ticksPerSixteenth, popupY = 0;
31         private boolean leftMouseButtonPressed = false, quantizeRecording = false;
32         protected static int viewLength = 0;
33         protected static int extraHeight = 0;
34         public static final int NOTE_HEIGHT = 10, NOTE_WIDTH = 40, VIEW_WIDTH = 200;
35
36         /**
37          * Creates the trackview.
38          * @param track The track it represents graphically and operates on.
39          * @param title The object that is used to manipulate instrument, channel, solo, mute.
40          */
41         public MooTrackView (Track track, MooTrackTitle title) {
42                 super(true);
43
44                 // Defines instance variables
45                 this.track = track;
46                 this.title = title;
47                 insets = getInsets();
48
49                 // Configures panel
50                 setBackground(Color.white);
51                 setBorder(BorderFactory.createLineBorder(Color.black));
52                 setLayout(null);
53                 setPreferredSize(new Dimension(VIEW_WIDTH, 140 * NOTE_HEIGHT));
54
55                 placeNoteElements(false);
56
57                 // Creates panel pop-up menu.
58                 popup = new JPopupMenu();
59                 popupAdd = addMenuItem(popup, "Add note...");
60                 popupPaste = addMenuItem(popup, "Paste");
61
62                 // Creates selection pop-up menu.
63                 selPopup = new JPopupMenu();
64                 selPopupCopy = addMenuItem(selPopup, "Copy selection");
65                 selPopupCut = addMenuItem(selPopup, "Cut selection");
66                 selPopupRemove = addMenuItem(selPopup, "Remove selection");
67                 selPopupTranspUpItems = new JMenuItem[12];
68                 selPopupTranspDownItems = new JMenuItem[12];
69                 selPopupTranspUp = createTransposeMenu(selPopup, selPopupTranspUpItems, "selection up");
70                 selPopupTranspDown = createTransposeMenu(selPopup, selPopupTranspDownItems, "selection down");
71
72                 // Adds listeners for popup menu and keyboard synthesizer.
73                 addMouseListener(new MAdapter());
74                 keyboard = new MooKeyboard(title);
75                 addKeyListener(keyboard);
76         }
77
78         /**
79          * Creates note elements for all MooNotes in the track, and places them in the appropriate place.
80          */
81         public void placeNoteElements(boolean quantize) {
82                 // Converts the track.
83                 Moosique.convertTrack(track);
84
85                 // Empties the container
86                 removeAll();
87                 coords = new ArrayList(track.size() / 2);
88                 selection = new TreeSet();
89
90                 // Creates temporary variables
91                 MidiEvent note;
92                 MooNoteElement elem;
93                 extraHeight = Toolkit.getDefaultToolkit().getScreenSize().height - 150;
94
95                 // Places note elements
96                 for (int i = 0; i < track.size(); i++) {
97                         note = track.get(i);
98                         if (note instanceof MooNote) {
99                                 // Adds the note element to the note area and moves it to the appropriate place.
100                                 MooNote mn = (MooNote)note;
101                                 elem = new MooNoteElement(this, mn);
102                                 add(elem);
103                                 layoutElement(elem, false);
104                         }
105                         setPreferredSize(new Dimension(VIEW_WIDTH, viewLength + extraHeight));
106                 }
107         }       
108
109         /**
110          * Layouts the element to the right place.
111          * @param elem  the element that will be layouted.
112          * @param old   If true, this method will remove the old layout and set the new preferredSize for the trackview.
113          */
114         public void layoutElement(MooNoteElement elem, boolean old){
115                 // If the element is currently in the view, removes its coordinates from the list.
116                 Rectangle r = new Rectangle();
117                 if (old){
118                         r = elem.getBounds(r);
119                         for (Iterator i = coords.iterator(); i.hasNext();){
120                                 Object ob = i.next();
121                                 if (r.equals(ob)){
122                                         coords.remove(ob);
123                                         break;
124                                 }
125                         }
126                 }
127
128                 // Creates temporary variables.
129                 ticksPerSixteenth = Moosique.getSequence().getResolution() / 4;
130                 MooNote mn = elem.getNote();
131                 int x, y, height;
132
133                 // Calculates coordinates.
134                 x = insets.left;
135                 if (quantizeRecording) {
136                         // Snap to nearest sixteenth
137                         y = insets.top + Math.round(mn.getTick() / ticksPerSixteenth) * NOTE_HEIGHT;
138                         height = (mn.getDuration() / ticksPerSixteenth) * NOTE_HEIGHT;
139                 } else {
140                         y = insets.top + (int)((mn.getTick() * NOTE_HEIGHT) / ticksPerSixteenth);
141                         height = (mn.getDuration() * NOTE_HEIGHT) / ticksPerSixteenth;
142                 }
143                 if (height == 0) height = NOTE_HEIGHT;
144                 r = new Rectangle(x, y, NOTE_WIDTH, height);
145
146                 // Places the element in the appropriate place.
147                 while(isOccupied(r)) r.translate(NOTE_WIDTH, 0);
148                 elem.setBounds(r);
149                 coords.add(r);
150                 if (viewLength < (y + height)){
151                         viewLength = y + height;
152                         if(old)setPreferredSize(new Dimension(VIEW_WIDTH, viewLength + extraHeight));
153                 }
154                 if (old) {
155                         validate();
156                         repaint();
157                 }
158         }
159
160         /** 
161          * Returns the track of this view.
162          * @return the track of this view
163          */
164         public Track getTrack() {
165                 return track;
166         }
167
168         /** 
169          * Returns the title of this view.
170          * @return the title of this view
171          */
172         public MooTrackTitle getTitle() {
173                 return title;
174         }
175
176         /**
177          * Checks if the element can be fully drawn as this position without inteferring with other elements.
178          * @return true if the position is occupied.
179          */
180         private boolean isOccupied(Rectangle r) {
181                 Iterator it = coords.iterator();
182                 while (it.hasNext()) {
183                         if(r.intersects((Rectangle)it.next())) return true;
184                 }
185                 return false;
186         }
187         
188         /** 
189          * Adds the given note to the current track, and visualises it.
190          * @param mn    the note to add
191          */
192         public void addNote(MooNote mn) {
193                 mn.addTo(track);
194                 MooNoteElement elem = new MooNoteElement(this, mn);
195                 add(elem);
196                 layoutElement(elem, false);
197                 setPreferredSize(new Dimension(VIEW_WIDTH, viewLength + extraHeight));
198                 Moosique.setEdited();
199                 validate();
200                 repaint();
201         }
202
203         /**
204          * Adds a standard note to this track.
205          */
206         private void addStandardNote() {
207                 int row = (popupY - insets.top) / NOTE_HEIGHT;
208                 long timestamp = (long)(ticksPerSixteenth * row);
209                 addNote(new MooNote(title.getChannel(), 60, 100, timestamp, Moosique.getSequence().getResolution() / 4));
210         }
211
212         /** 
213          * Removes the given note element from the view and its note from the current track.
214          * @param elem  the note element to remove
215          */
216         public void removeNote(MooNoteElement elem) {
217                 elem.getNote().removeFrom(track);
218                 remove(elem);
219                 Rectangle r = new Rectangle();
220                 r = elem.getBounds(r);
221                 coords.remove(r);
222                 Moosique.setEdited();
223                 validate();
224                 repaint();
225         }
226
227         /**
228          * Selects the given note
229          * @param the note to select
230          */
231         public void selectNote(MooNoteElement elem) {
232                 selection.add(elem);
233         }
234
235         /**
236          * Deselects the given note
237          * @param the note to deselect
238          */
239         public void deselectNote(MooNoteElement elem) {
240                 selection.remove(elem);
241         }
242
243         /**
244          * Deselects all notes.
245          */
246         public void deselectAllNotes() {
247                 Iterator it = selection.iterator();
248                 while(it.hasNext()) {
249                         ((MooNoteElement)it.next()).deselect();
250                 }
251                 selection.clear();
252         }
253
254         /**
255          * Determines if the given MooNoteElement is the only one in the track view that is selected.
256          * @return if the given element is the only selected one
257          */
258         public boolean isTheOnlySelected(MooNoteElement elem) {
259                 Iterator it = selection.iterator();
260                 while(it.hasNext()) {
261                         if (!it.next().equals(elem)) return false;
262                 }
263                 return true;
264         }
265
266         /**
267          * Copies the current selection.
268          */
269         public void copySelectedNotes() {
270                 ArrayList copyBuffer = new ArrayList(selection.size());
271                 Iterator it = selection.iterator();
272                 while(it.hasNext()) {
273                         copyBuffer.add(((MooNoteElement)it.next()).getNote().clone());
274                 }
275                 Collections.sort(copyBuffer);
276                 Moosique.setCopyBuffer(copyBuffer);
277         }
278
279         /**
280          * Cuts the current selection.
281          */
282         public void cutSelectedNotes() {
283                 copySelectedNotes();
284                 removeSelectedNotes();
285         }
286
287         /**
288          * Pastes the current copy buffer at the given timestamp.
289          */
290         public void pasteCopiedNotes() {
291                 int row = (popupY - insets.top) / NOTE_HEIGHT;
292                 long timestamp = (long)(ticksPerSixteenth * row);
293                 ArrayList copyBuffer = Moosique.getCopyBuffer();
294                 if (copyBuffer.size() > 0) {
295                         long startTime = ((MooNote)copyBuffer.get(0)).getTick();
296                         Iterator it = copyBuffer.iterator();
297                         while(it.hasNext()) {
298                                 MooNote mn = (MooNote)((MooNote)it.next()).clone();
299                                 mn.setTick(mn.getTick() - startTime + timestamp);
300                                 mn.setChannel(title.getChannel());
301                                 addNote(mn);
302                         }
303                 }
304         }
305
306         /**
307          * Removes the current selection.
308          */
309         public void removeSelectedNotes() {
310                 Iterator it = selection.iterator();
311                 while(it.hasNext()) {
312                         removeNote((MooNoteElement)it.next());
313                 }
314                 selection.clear();
315         }
316
317         /**
318          * Transposes all selected notes the given number of halftones.
319          */
320         private void transposeSelectedNotes(int halftones) {
321                 Iterator it = selection.iterator();
322                 while(it.hasNext()) {
323                         MooNoteElement elem = (MooNoteElement)it.next();
324                         elem.transpose(halftones);
325                 }
326         }
327
328         /**
329          * Moves the current selection the given number of ticks.
330          * @param ticks         the number of ticks to move the selection.
331          */
332         public void moveSelectedNotes(int ticks) {
333                 if (ticks < 0) {
334                         // If the selection should be moved upwards, traverses the list in the natural order.
335                         Iterator it = selection.iterator();
336                         while(it.hasNext()) {
337                                 MooNoteElement elem = (MooNoteElement)it.next();
338                                 elem.getNote().setTick(elem.getNote().getTick() + ticks);
339                                 layoutElement(elem, true);
340                         }
341                 } else {
342                         // If the selection should be moved downwards, traverses the list in the opposite order.
343                         ArrayList selectedList = new ArrayList(selection);
344                         ListIterator it = selectedList.listIterator(selectedList.size());
345                         while(it.hasPrevious()) {
346                                 MooNoteElement elem = (MooNoteElement)it.previous();
347                                 elem.getNote().setTick(elem.getNote().getTick() + ticks);
348                                 layoutElement(elem, true);
349                         }
350                 }
351         }
352
353         /**
354          * Moves the current selection, depending on the given delta y.
355          * @param y     the number of pixels the selection is moved.
356          */
357         public void maybeMoveSelectedNotes(int y) {
358                 moveSelectedNotes(ticksPerSixteenth * (y / NOTE_HEIGHT));
359         }
360
361         /**
362          * Enables keyboard recording.
363          */
364         public void enableKeyboardRecording() {
365                 keyboard.recordEnable();
366         }
367
368         /**
369          * Disables keyboard recording.
370          */
371         public void disableKeyboardRecording() {
372                 keyboard.recordDisable();
373         }
374
375         /**
376          * Adds a menu item with the given command to the given popup menu.
377          */
378         private JMenuItem addMenuItem(JPopupMenu menu, String command) {
379                 JMenuItem item = new JMenuItem(command);
380                 item.addActionListener(new PopupListener());
381                 menu.add(item);
382                 return item;
383         }
384
385         /**
386          * Adds a menu item with the given command to the given menu.
387          */
388         private JMenuItem addMenuItem(JMenu menu, String command) {
389                 JMenuItem item = new JMenuItem(command);
390                 item.addActionListener(new PopupListener());
391                 menu.add(item);
392                 return item;
393         }
394
395         /**
396          * Creates a transpose sub menu with the given title in the given popup menu,
397          * inserting the items into the given array.
398          */
399         private JMenu createTransposeMenu(JPopupMenu menu, JMenuItem[] items, String title) {
400                 JMenu trans = new JMenu("Transpose " + title);
401                 menu.add(trans);
402                 items[0] = addMenuItem(trans, "One octave");
403                 for (int i = 1; i < 12; i++) {
404                         items[i] = addMenuItem(trans, (i) + " halftones");
405                 }
406                 return trans;
407         }
408
409         /**
410          * Shows a popup-menu with options for the current selection of note elements.
411          * @param c     the component over which to display the menu
412          * @param x     the x-coordinate in which to display the menu
413          * @param y     the y-coordinate in which to display the menu
414          */
415         public void showSelectionPopup(Component c, int x, int y) {
416                 selPopup.show(c, x, y);
417         }
418
419         /**
420          * Draws the grid that is on the background.
421          * @param g The Graphics object used to draw the grid.
422          */
423         public void paintComponent(Graphics g) {
424                 super.paintComponent(g);
425                 Graphics2D g2 = (Graphics2D)g;
426                 for (int c = 0; c < viewLength || c < getHeight(); c += NOTE_HEIGHT) {
427                         for (int r = 0; r < (10 * NOTE_WIDTH); r += NOTE_WIDTH) {
428                                 box = new Rectangle(r, c, NOTE_WIDTH, NOTE_HEIGHT);
429                                 g2.setColor(Color.gray);
430                                 g2.draw(box);
431                         }
432                 }
433         }
434         
435         /**
436          * Returns whether the left mouse button is currently pressed or not.
437          * @return true if the left mosue button is currently pressed
438          */
439         public boolean isLeftMouseButtonPressed() {
440                 return leftMouseButtonPressed;
441         }
442
443         /**
444          * The adapter used to listen on mouse actions
445          */
446         class MAdapter extends MouseAdapter {
447
448                 /**
449                  * Deselects all note on click, adds a standard note on double click.
450                  */
451                 public void mouseClicked(MouseEvent e) {
452                         if (SwingUtilities.isLeftMouseButton(e)) {
453                                 deselectAllNotes();
454                                 if (e.getClickCount() == 2) {
455                                         popupY = e.getY();
456                                         addStandardNote();
457                                 }
458                         }
459                 }
460         
461                 public void mousePressed(MouseEvent e) {
462                         if (SwingUtilities.isLeftMouseButton(e)) leftMouseButtonPressed = true;
463                         maybeShowPopup(e);
464                 }
465
466                 public void mouseReleased(MouseEvent e) {
467                         if (SwingUtilities.isLeftMouseButton(e)) leftMouseButtonPressed = false;
468                         maybeShowPopup(e);
469                 }
470
471                 /**
472                  * Shows the menu if an OS-specific popup-trigger was activated.
473                  */
474                 private void maybeShowPopup(MouseEvent e) {
475                         if (e.isPopupTrigger()) {
476                                 popupY = e.getY();
477                                 popup.show(e.getComponent(), e.getX(), e.getY());
478                         }
479                 }
480
481                 /**
482                  * Grabs the focus when the mouse has entered.
483                  */
484                 public void mouseEntered(MouseEvent e) {
485                         Moosique.setActiveChannel(title.getChannel());
486                         grabFocus();
487                 }
488         }
489
490         /**
491          * Takes the appropriate action when a user selects an item on the popup menu.
492          */
493         class PopupListener implements ActionListener {
494                 public void actionPerformed(ActionEvent e) {
495                         Object source = e.getSource();
496                         // Handling panel popup actions.
497                         if (source == popupAdd) {
498                                 addStandardNote();
499                         } else if (source == popupPaste) {
500                                 pasteCopiedNotes();
501                         // Handling selection popup actions.
502                         } else if (source == selPopupCopy) {
503                                 copySelectedNotes();
504                         } else if (source == selPopupCut) {
505                                 cutSelectedNotes();
506                         } else if (source == selPopupRemove) {
507                                 removeSelectedNotes();
508                         } else if (source == selPopupTranspUpItems[0]) {
509                                 transposeSelectedNotes(12);
510                         } else if (source == selPopupTranspDownItems[0]) {
511                                 transposeSelectedNotes(-12);
512                         } else {
513                                 for (int i = 1; i < 12; i++) {
514                                         if (source == selPopupTranspUpItems[i]) transposeSelectedNotes(i);
515                                         else if (source == selPopupTranspDownItems[i]) transposeSelectedNotes(-i);
516                                 }
517                         }
518                 }
519         }
520 }