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