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