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