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