]> ruin.nu Git - moosique.git/blob - MooTrackView.java
Fixed drag select in track view.
[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 Rectangle box;
19
20         private JPopupMenu popup, selPopup;
21         private JMenu selPopupTranspUp, selPopupTranspDown;
22         private JMenuItem popupAdd, popupPaste;
23         private JMenuItem selPopupCopy, selPopupCut, selPopupRemove, selPopupTranspUpOct, selPopupTranspDownOct;
24
25         private ArrayList coords, selection, copyBuffer; 
26         private Insets insets;
27         private int ticksPerSixteenth, popupY = 0;
28         private boolean leftMouseButtonPressed = false;
29         protected static int viewLength = 0;
30         protected static int extraHeight = 0;
31         public static final int NOTE_HEIGHT = 10, NOTE_WIDTH = 40, VIEW_WIDTH = 200;
32
33         /**
34          * Creates the trackview.
35          * @param track The track it represents graphically and operates on.
36          * @param title The object that is used to manipulate instrument, channel, solo, mute.
37          */
38         public MooTrackView (Track track, MooTrackTitle title) {
39                 super(true);
40                 this.track = track;
41                 this.title = title;
42
43                 // Creates instance variables
44                 insets = getInsets();
45                 coords = new ArrayList(track.size() / 2);
46                 selection = new ArrayList();
47                 copyBuffer = new ArrayList();
48
49                 // Creates temporary variables
50                 MidiEvent note;
51                 MooNoteElement elem;
52                 extraHeight = Toolkit.getDefaultToolkit().getScreenSize().height - 150;
53
54                 // Configures panel
55                 setBackground(Color.white);
56                 setBorder(BorderFactory.createLineBorder(Color.black));
57                 setLayout(null);
58                 setPreferredSize(new Dimension(VIEW_WIDTH, 140 * NOTE_HEIGHT));
59
60                 // Places note elements
61                 for (int i = 0; i < track.size(); i++) {
62                         note = track.get(i);
63                         if (note instanceof MooNote) {
64                                 // Adds the note element to the note area and moves it to the appropriate place.
65                                 MooNote mn = (MooNote)note;
66                                 elem = new MooNoteElement(this, mn);
67                                 add(elem);
68                                 layoutElement(elem, false);
69                         }
70                         setPreferredSize(new Dimension(VIEW_WIDTH, viewLength + extraHeight));
71
72                 }
73
74                 // Creates panel pop-up menu.
75                 popup = new JPopupMenu();
76                 PopupListener pList = new PopupListener();
77                 popupAdd = new JMenuItem("Add note...");
78                 popupAdd.addActionListener(pList);
79                 popup.add(popupAdd);
80                 popupPaste = new JMenuItem("Paste");
81                 popupPaste.addActionListener(pList);
82                 popup.add(popupPaste);
83
84                 // Creates selection pop-up menu.
85                 selPopup = new JPopupMenu();
86                 selPopupCopy = new JMenuItem("Copy selection");
87                 selPopupCopy.addActionListener(pList);
88                 selPopup.add(selPopupCopy);
89                 selPopupCut = new JMenuItem("Cut selection");
90                 selPopupCut.addActionListener(pList);
91                 selPopup.add(selPopupCut);
92                 selPopupRemove = new JMenuItem("Remove selection");
93                 selPopupRemove.addActionListener(pList);
94                 selPopup.add(selPopupRemove);
95                 selPopupTranspUp = new JMenu("Transpose selection up");
96                 selPopup.add(selPopupTranspUp);
97                 selPopupTranspUpOct = new JMenuItem("One octave");
98                 selPopupTranspUpOct.addActionListener(pList);
99                 selPopupTranspUp.add(selPopupTranspUpOct);
100                 selPopupTranspDown = new JMenu("Transpose selection down");
101                 selPopup.add(selPopupTranspDown);
102                 selPopupTranspDownOct = new JMenuItem("One octave");
103                 selPopupTranspDownOct.addActionListener(pList);
104                 selPopupTranspDown.add(selPopupTranspDownOct);
105
106                 // Adds listeners for popup menu and keyboard synthesizer.
107                 addMouseListener(new MAdapter());
108                 addKeyListener(new MooKeyboard());
109         }
110
111         /**
112          * Layouts the element to the right place.
113          * @param elem  the element that will be layouted.
114          * @param old   If true, this method will remove the old layout and set the new preferredSize for the trackview.
115          */
116         public void layoutElement(MooNoteElement elem, boolean old){
117                 // If the element is currently in the view, removes its coordinates from the list.
118                 Rectangle r = new Rectangle();
119                 if (old){
120                         r = elem.getBounds(r);
121                         for (Iterator i = coords.iterator(); i.hasNext();){
122                                 Object ob = i.next();
123                                 if (r.equals(ob)){
124                                         coords.remove(ob);
125                                         break;
126                                 }
127                         }
128                 }
129
130                 // Creates temporary variables.
131                 ticksPerSixteenth = Moosique.getSequence().getResolution() / 4;
132                 MooNote mn = elem.getNote();
133                 int x, y, height;
134
135                 // Calculates coordinates.
136                 x = insets.left;
137                 y = insets.top + (int)(mn.getTick() / ticksPerSixteenth) * NOTE_HEIGHT;
138                 height = (mn.getDuration() / ticksPerSixteenth) * NOTE_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(selection.indexOf(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                 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, new Moosique.NoteComparator());
272         }
273
274         /**
275          * Cuts the current selection.
276          */
277         public void cutSelectedNotes() {
278                 copySelectedNotes();
279                 removeSelectedNotes();
280         }
281
282         /**
283          * Pastes the current copy buffer at the given timestamp.
284          */
285         public void pasteCopiedNotes() {
286                 int row = (popupY - insets.top) / NOTE_HEIGHT;
287                 long timestamp = (long)(ticksPerSixteenth * row);
288                 if (copyBuffer.size() > 0) {
289                         long startTime = ((MooNote)copyBuffer.get(0)).getTick();
290                         Iterator it = copyBuffer.iterator();
291                         while(it.hasNext()) {
292                                 MooNote mn = (MooNote)((MooNote)it.next()).clone();
293                                 mn.setTick(mn.getTick() - startTime + timestamp);
294                                 addNote(mn);
295                         }
296                 }
297         }
298
299         /**
300          * Removes the current selection.
301          */
302         public void removeSelectedNotes() {
303                 Iterator it = selection.iterator();
304                 while(it.hasNext()) {
305                         removeNote((MooNoteElement)it.next());
306                 }
307                 selection.clear();
308         }
309
310         /**
311          * Transposes all selected notes the given number of halftones.
312          */
313         private void transposeSelectedNotes(int halftones) {
314                 Iterator it = selection.iterator();
315                 while(it.hasNext()) {
316                         MooNoteElement elem = (MooNoteElement)it.next();
317                         elem.transpose(halftones);
318                 }
319         }
320
321         /**
322          * Shows a popup-menu with options for the current selection of note elements.
323          * @param c     the component over which to display the menu
324          * @param x     the x-coordinate in which to display the menu
325          * @param y     the y-coordinate in which to display the menu
326          */
327         public void showSelectionPopup(Component c, int x, int y) {
328                 selPopup.show(c, x, y);
329         }
330
331         /**
332          * Draws the grid that is on the background.
333          * @param g The Graphics object used to draw the grid.
334          */
335         public void paintComponent(Graphics g) {
336                 super.paintComponent(g);
337                 Graphics2D g2 = (Graphics2D)g;
338                 for (int c = 0; c < viewLength || c < getHeight(); c += NOTE_HEIGHT) {
339                         for (int r = 0; r < (10 * NOTE_WIDTH); r += NOTE_WIDTH) {
340                                 box = new Rectangle(r, c, NOTE_WIDTH, NOTE_HEIGHT);
341                                 g2.setColor(Color.gray);
342                                 g2.draw(box);
343                         }
344                 }
345         }
346         
347         /**
348          * Returns whether the left mouse button is currently pressed or not.
349          * @return true if the left mosue button is currently pressed
350          */
351         public boolean isLeftMouseButtonPressed() {
352                 return leftMouseButtonPressed;
353         }
354
355         /**
356          * The adapter used to listen on mouse actions
357          */
358         class MAdapter extends MouseAdapter {
359
360                 /**
361                  * Deselects all note on click, adds a standard note on double click.
362                  */
363                 public void mouseClicked(MouseEvent e) {
364                         if (SwingUtilities.isLeftMouseButton(e)) {
365                                 deselectAllNotes();
366                                 if (e.getClickCount() == 2) {
367                                         popupY = e.getY();
368                                         addStandardNote();
369                                 }
370                         }
371                 }
372         
373                 public void mousePressed(MouseEvent e) {
374                         if (SwingUtilities.isLeftMouseButton(e)) leftMouseButtonPressed = true;
375                         maybeShowPopup(e);
376                 }
377
378                 public void mouseReleased(MouseEvent e) {
379                         if (SwingUtilities.isLeftMouseButton(e)) leftMouseButtonPressed = false;
380                         maybeShowPopup(e);
381                 }
382
383                 /**
384                  * Selects the notes within the area that was selected.
385                  */
386                 public void mouseDragged(MouseEvent e) {
387                         
388                 }
389
390                 /**
391                  * Shows the menu if an OS-specific popup-trigger was activated.
392                  */
393                 private void maybeShowPopup(MouseEvent e) {
394                         if (e.isPopupTrigger()) {
395                                 popupY = e.getY();
396                                 popup.show(e.getComponent(), e.getX(), e.getY());
397                         }
398                 }
399
400                 /**
401                  * Grabs the focus when the mouse has entered.
402                  */
403                 public void mouseEntered(MouseEvent e) {
404                         Moosique.setActiveChannel(title.getChannel());
405                         grabFocus();
406                 }
407         }
408
409         /**
410          * Listens on actions on the popup menu and executes the appropriate action.
411          */
412         class PopupListener implements ActionListener {
413                 public void actionPerformed(ActionEvent e) {
414                         Object source = e.getSource();
415                         // Handling panel popup actions.
416                         if (source == popupAdd) {
417                                 addStandardNote();
418                         } else if (source == popupPaste) {
419                                 pasteCopiedNotes();
420                         // Handling selection popup actions.
421                         } else if (source == selPopupCopy) {
422                                 copySelectedNotes();
423                         } else if (source == selPopupCut) {
424                                 cutSelectedNotes();
425                         } else if (source == selPopupRemove) {
426                                 removeSelectedNotes();
427                         } else if (source == selPopupTranspUpOct) {
428                                 transposeSelectedNotes(12);
429                         } else if (source == selPopupTranspDownOct) {
430                                 transposeSelectedNotes(-12);
431                         }
432                 }
433         }
434 }