]> ruin.nu Git - moosique.git/blob - MooTrackView.java
0c48df37ad736b7ccc2d2dbc6c3c9545c498f7d4
[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         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();
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() {
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                 y = insets.top + (int)((mn.getTick() * NOTE_HEIGHT) / ticksPerSixteenth);
136                 height = (mn.getDuration() * NOTE_HEIGHT) / ticksPerSixteenth;
137                 System.out.println("y=i(" + insets.top + ")+t(" + mn.getTick() + ")/s(" + ticksPerSixteenth + ") *n(" + NOTE_HEIGHT + ") = " + y);
138                 System.out.println("h=t(" + mn.getDuration() + ")/s(" + ticksPerSixteenth + ") *n(" + NOTE_HEIGHT + ") = " + height);
139                 if (height == 0) height = NOTE_HEIGHT;
140                 r = new Rectangle(x, y, NOTE_WIDTH, height);
141
142                 // Places the element in the appropriate place.
143                 while(isOccupied(r)) r.translate(NOTE_WIDTH, 0);
144                 elem.setBounds(r);
145                 coords.add(r);
146                 if (viewLength < (y + height)){
147                         viewLength = y + height;
148                         if(old)setPreferredSize(new Dimension(VIEW_WIDTH, viewLength + extraHeight));
149                 }
150                 if (old) {
151                         validate();
152                         repaint();
153                 }
154         }
155
156         /** 
157          * Returns the track of this view.
158          * @return the track of this view
159          */
160         public Track getTrack() {
161                 return track;
162         }
163
164         /** 
165          * Returns the title of this view.
166          * @return the title of this view
167          */
168         public MooTrackTitle getTitle() {
169                 return title;
170         }
171
172         /**
173          * Checks if the element can be fully drawn as this position without inteferring with other elements.
174          * @return true if the position is occupied.
175          */
176         private boolean isOccupied(Rectangle r) {
177                 Iterator it = coords.iterator();
178                 while (it.hasNext()) {
179                         if(r.intersects((Rectangle)it.next())) return true;
180                 }
181                 return false;
182         }
183         
184         /** 
185          * Adds the given note to the current track, and visualises it.
186          * @param mn    the note to add
187          */
188         public void addNote(MooNote mn) {
189                 mn.addTo(track);
190                 MooNoteElement elem = new MooNoteElement(this, mn);
191                 add(elem);
192                 layoutElement(elem, false);
193                 setPreferredSize(new Dimension(VIEW_WIDTH, viewLength + extraHeight));
194                 Moosique.setEdited();
195                 validate();
196                 repaint();
197         }
198
199         /**
200          * Adds a standard note to this track.
201          */
202         private void addStandardNote() {
203                 int row = (popupY - insets.top) / NOTE_HEIGHT;
204                 long timestamp = (long)(ticksPerSixteenth * row);
205                 addNote(new MooNote(title.getChannel(), 60, 100, timestamp, Moosique.getSequence().getResolution() / 4));
206         }
207
208         /** 
209          * Removes the given note element from the view and its note from the current track.
210          * @param elem  the note element to remove
211          */
212         public void removeNote(MooNoteElement elem) {
213                 elem.getNote().removeFrom(track);
214                 remove(elem);
215                 Rectangle r = new Rectangle();
216                 r = elem.getBounds(r);
217                 coords.remove(r);
218                 Moosique.setEdited();
219                 validate();
220                 repaint();
221         }
222
223         /**
224          * Selects the given note
225          * @param the note to select
226          */
227         public void selectNote(MooNoteElement elem) {
228                 selection.add(elem);
229         }
230
231         /**
232          * Deselects the given note
233          * @param the note to deselect
234          */
235         public void deselectNote(MooNoteElement elem) {
236                 selection.remove(elem);
237         }
238
239         /**
240          * Deselects all notes.
241          */
242         public void deselectAllNotes() {
243                 Iterator it = selection.iterator();
244                 while(it.hasNext()) {
245                         ((MooNoteElement)it.next()).deselect();
246                 }
247                 selection.clear();
248         }
249
250         /**
251          * Determines if the given MooNoteElement is the only one in the track view that is selected.
252          * @return if the given element is the only selected one
253          */
254         public boolean isTheOnlySelected(MooNoteElement elem) {
255                 Iterator it = selection.iterator();
256                 while(it.hasNext()) {
257                         if (!it.next().equals(elem)) return false;
258                 }
259                 return true;
260         }
261
262         /**
263          * Copies the current selection.
264          */
265         public void copySelectedNotes() {
266                 ArrayList copyBuffer = new ArrayList(selection.size());
267                 Iterator it = selection.iterator();
268                 while(it.hasNext()) {
269                         copyBuffer.add(((MooNoteElement)it.next()).getNote().clone());
270                 }
271                 Collections.sort(copyBuffer);
272                 Moosique.setCopyBuffer(copyBuffer);
273         }
274
275         /**
276          * Cuts the current selection.
277          */
278         public void cutSelectedNotes() {
279                 copySelectedNotes();
280                 removeSelectedNotes();
281         }
282
283         /**
284          * Pastes the current copy buffer at the given timestamp.
285          */
286         public void pasteCopiedNotes() {
287                 int row = (popupY - insets.top) / NOTE_HEIGHT;
288                 long timestamp = (long)(ticksPerSixteenth * row);
289                 ArrayList copyBuffer = Moosique.getCopyBuffer();
290                 if (copyBuffer.size() > 0) {
291                         long startTime = ((MooNote)copyBuffer.get(0)).getTick();
292                         Iterator it = copyBuffer.iterator();
293                         while(it.hasNext()) {
294                                 MooNote mn = (MooNote)((MooNote)it.next()).clone();
295                                 mn.setTick(mn.getTick() - startTime + timestamp);
296                                 mn.setChannel(title.getChannel());
297                                 addNote(mn);
298                         }
299                 }
300         }
301
302         /**
303          * Removes the current selection.
304          */
305         public void removeSelectedNotes() {
306                 Iterator it = selection.iterator();
307                 while(it.hasNext()) {
308                         removeNote((MooNoteElement)it.next());
309                 }
310                 selection.clear();
311         }
312
313         /**
314          * Transposes all selected notes the given number of halftones.
315          */
316         private void transposeSelectedNotes(int halftones) {
317                 Iterator it = selection.iterator();
318                 while(it.hasNext()) {
319                         MooNoteElement elem = (MooNoteElement)it.next();
320                         elem.transpose(halftones);
321                 }
322         }
323
324         /**
325          * Moves the current selection the given number of ticks.
326          * @param ticks         the number of ticks to move the selection.
327          */
328         public void moveSelectedNotes(int ticks) {
329                 if (ticks < 0) {
330                         // If the selection should be moved upwards, traverses the list in the natural order.
331                         Iterator it = selection.iterator();
332                         while(it.hasNext()) {
333                                 MooNoteElement elem = (MooNoteElement)it.next();
334                                 elem.getNote().setTick(elem.getNote().getTick() + ticks);
335                                 layoutElement(elem, true);
336                         }
337                 } else {
338                         // If the selection should be moved downwards, traverses the list in the opposite order.
339                         ArrayList selectedList = new ArrayList(selection);
340                         ListIterator it = selectedList.listIterator(selectedList.size());
341                         while(it.hasPrevious()) {
342                                 MooNoteElement elem = (MooNoteElement)it.previous();
343                                 elem.getNote().setTick(elem.getNote().getTick() + ticks);
344                                 layoutElement(elem, true);
345                         }
346                 }
347         }
348
349         /**
350          * Moves the current selection, depending on the given delta y.
351          * @param y     the number of pixels the selection is moved.
352          */
353         public void maybeMoveSelectedNotes(int y) {
354                 moveSelectedNotes(ticksPerSixteenth * (y / NOTE_HEIGHT));
355         }
356
357         /**
358          * Enables keyboard recording.
359          */
360         public void enableKeyboardRecording() {
361                 keyboard.recordEnable();
362         }
363
364         /**
365          * Disables keyboard recording.
366          */
367         public void disableKeyboardRecording() {
368                 keyboard.recordDisable();
369         }
370
371         /**
372          * Adds a menu item with the given command to the given popup menu.
373          */
374         private JMenuItem addMenuItem(JPopupMenu menu, String command) {
375                 JMenuItem item = new JMenuItem(command);
376                 item.addActionListener(new PopupListener());
377                 menu.add(item);
378                 return item;
379         }
380
381         /**
382          * Adds a menu item with the given command to the given menu.
383          */
384         private JMenuItem addMenuItem(JMenu menu, String command) {
385                 JMenuItem item = new JMenuItem(command);
386                 item.addActionListener(new PopupListener());
387                 menu.add(item);
388                 return item;
389         }
390
391         /**
392          * Creates a transpose sub menu with the given title in the given popup menu,
393          * inserting the items into the given array.
394          */
395         private JMenu createTransposeMenu(JPopupMenu menu, JMenuItem[] items, String title) {
396                 JMenu trans = new JMenu("Transpose " + title);
397                 menu.add(trans);
398                 items[0] = addMenuItem(trans, "One octave");
399                 for (int i = 1; i < 12; i++) {
400                         items[i] = addMenuItem(trans, (i) + " halftones");
401                 }
402                 return trans;
403         }
404
405         /**
406          * Shows a popup-menu with options for the current selection of note elements.
407          * @param c     the component over which to display the menu
408          * @param x     the x-coordinate in which to display the menu
409          * @param y     the y-coordinate in which to display the menu
410          */
411         public void showSelectionPopup(Component c, int x, int y) {
412                 selPopup.show(c, x, y);
413         }
414
415         /**
416          * Draws the grid that is on the background.
417          * @param g The Graphics object used to draw the grid.
418          */
419         public void paintComponent(Graphics g) {
420                 super.paintComponent(g);
421                 Graphics2D g2 = (Graphics2D)g;
422                 for (int c = 0; c < viewLength || c < getHeight(); c += NOTE_HEIGHT) {
423                         for (int r = 0; r < (10 * NOTE_WIDTH); r += NOTE_WIDTH) {
424                                 box = new Rectangle(r, c, NOTE_WIDTH, NOTE_HEIGHT);
425                                 g2.setColor(Color.gray);
426                                 g2.draw(box);
427                         }
428                 }
429         }
430         
431         /**
432          * Returns whether the left mouse button is currently pressed or not.
433          * @return true if the left mosue button is currently pressed
434          */
435         public boolean isLeftMouseButtonPressed() {
436                 return leftMouseButtonPressed;
437         }
438
439         /**
440          * The adapter used to listen on mouse actions
441          */
442         class MAdapter extends MouseAdapter {
443
444                 /**
445                  * Deselects all note on click, adds a standard note on double click.
446                  */
447                 public void mouseClicked(MouseEvent e) {
448                         if (SwingUtilities.isLeftMouseButton(e)) {
449                                 deselectAllNotes();
450                                 if (e.getClickCount() == 2) {
451                                         popupY = e.getY();
452                                         addStandardNote();
453                                 }
454                         }
455                 }
456         
457                 public void mousePressed(MouseEvent e) {
458                         if (SwingUtilities.isLeftMouseButton(e)) leftMouseButtonPressed = true;
459                         maybeShowPopup(e);
460                 }
461
462                 public void mouseReleased(MouseEvent e) {
463                         if (SwingUtilities.isLeftMouseButton(e)) leftMouseButtonPressed = false;
464                         maybeShowPopup(e);
465                 }
466
467                 /**
468                  * Shows the menu if an OS-specific popup-trigger was activated.
469                  */
470                 private void maybeShowPopup(MouseEvent e) {
471                         if (e.isPopupTrigger()) {
472                                 popupY = e.getY();
473                                 popup.show(e.getComponent(), e.getX(), e.getY());
474                         }
475                 }
476
477                 /**
478                  * Grabs the focus when the mouse has entered.
479                  */
480                 public void mouseEntered(MouseEvent e) {
481                         Moosique.setActiveChannel(title.getChannel());
482                         grabFocus();
483                 }
484         }
485
486         /**
487          * Takes the appropriate action when a user selects an item on the popup menu.
488          */
489         class PopupListener implements ActionListener {
490                 public void actionPerformed(ActionEvent e) {
491                         Object source = e.getSource();
492                         // Handling panel popup actions.
493                         if (source == popupAdd) {
494                                 addStandardNote();
495                         } else if (source == popupPaste) {
496                                 pasteCopiedNotes();
497                         // Handling selection popup actions.
498                         } else if (source == selPopupCopy) {
499                                 copySelectedNotes();
500                         } else if (source == selPopupCut) {
501                                 cutSelectedNotes();
502                         } else if (source == selPopupRemove) {
503                                 removeSelectedNotes();
504                         } else if (source == selPopupTranspUpItems[0]) {
505                                 transposeSelectedNotes(12);
506                         } else if (source == selPopupTranspDownItems[0]) {
507                                 transposeSelectedNotes(-12);
508                         } else {
509                                 for (int i = 1; i < 12; i++) {
510                                         if (source == selPopupTranspUpItems[i]) transposeSelectedNotes(i);
511                                         else if (source == selPopupTranspDownItems[i]) transposeSelectedNotes(-i);
512                                 }
513                         }
514                 }
515         }
516 }