Java tutorial
/** * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.datavyu.controllers.component; import com.google.common.collect.Maps; import com.usermetrix.jclient.Logger; import com.usermetrix.jclient.UserMetrix; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.List; import java.util.Map; import javax.swing.BorderFactory; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.event.EventListenerList; import javax.swing.event.MouseInputAdapter; import net.miginfocom.swing.MigLayout; import org.apache.commons.lang.text.StrSubstitutor; import org.datavyu.Datavyu; import org.datavyu.event.component.CarriageEvent; import org.datavyu.event.component.CarriageEventListener; import org.datavyu.event.component.TrackMouseEventListener; import org.datavyu.event.component.CarriageEvent.EventType; import org.datavyu.models.component.MixerModel; import org.datavyu.models.component.TrackConstants; import org.datavyu.models.component.TrackModel; import org.datavyu.models.component.ViewportState; import org.datavyu.models.component.TrackModel.TrackState; import org.datavyu.models.id.Identifier; import org.datavyu.plugins.CustomActions; import org.datavyu.plugins.ViewerStateListener; import org.datavyu.views.DataControllerV; import org.datavyu.views.component.TrackPainter; /** * TrackPainterController is responsible for managing a TrackPainter. */ public final class TrackController implements ViewerStateListener, PropertyChangeListener { /** The UserMetrix logger for this class. */ private static final Logger LOGGER = UserMetrix.getLogger(TrackController.class); /** Main panel holding the track UI. */ private final JPanel view; /** Header block. */ private final JPanel header; /** Track label. */ private final JLabel trackLabel; /** Label holding the icon. */ private final JLabel iconLabel; /** Component that paints the track. */ private final TrackPainter trackPainter; /** Right click menu. */ private final JPopupMenu menu; private final JMenuItem setBookmarkMenuItem; private final JMenuItem clearBookmarkMenuItem; /** Button for (un)locking the track. */ private final JButton lockUnlockButton; /** Button for unloading the track (and its associated plugin). */ private final JButton rubbishButton; /** Button for hiding or showing the data viewer. */ private final JButton visibleButton; /** Viewable model. */ private final MixerModel mixerModel; /** Track model. */ private final TrackModel trackModel; /** * Listeners interested in custom playback region events and mouse events on * the track. */ private final EventListenerList listenerList; /** States. */ // can the carriage be moved using the mouse when snap is switched on private boolean isMoveable; private boolean isViewerVisible = true; /** * Creates a new TrackController. * * @param trackPainter the track painter for this controller to manage. */ public TrackController(final MixerModel mixerModel, final TrackPainter trackPainter) { isMoveable = true; view = new JPanel(); view.setLayout(new MigLayout("fillx, ins 0", "[]0[]")); view.setBorder(BorderFactory.createLineBorder(TrackConstants.BORDER_COLOR, 1)); this.trackPainter = trackPainter; this.mixerModel = mixerModel; trackModel = new TrackModel(); trackModel.setState(TrackState.NORMAL); trackModel.clearBookmarks(); trackModel.setLocked(false); trackPainter.setMixerView(mixerModel); trackPainter.setTrackModel(trackModel); mixerModel.getViewportModel().addPropertyChangeListener(this); listenerList = new EventListenerList(); final TrackPainterListener painterListener = new TrackPainterListener(); trackPainter.addMouseListener(painterListener); trackPainter.addMouseMotionListener(painterListener); menu = new JPopupMenu(); menu.setName("trackPopUpMenu"); setBookmarkMenuItem = new JMenuItem("Set bookmark"); setBookmarkMenuItem.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { TrackController.this.setBookmarkAction(); } }); clearBookmarkMenuItem = new JMenuItem("Clear bookmarks"); clearBookmarkMenuItem.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { TrackController.this.clearBookmarkAction(); } }); menu.add(setBookmarkMenuItem); menu.add(clearBookmarkMenuItem); trackPainter.add(menu); // Create the Header panel and its components trackLabel = new JLabel("", SwingConstants.CENTER); trackLabel.setName("trackLabel"); trackLabel.setHorizontalAlignment(SwingConstants.CENTER); trackLabel.setHorizontalTextPosition(SwingConstants.CENTER); iconLabel = new JLabel("", SwingConstants.CENTER); iconLabel.setHorizontalAlignment(SwingConstants.CENTER); iconLabel.setHorizontalTextPosition(SwingConstants.CENTER); header = new JPanel(new MigLayout("ins 0, wrap 6")); header.setBorder(BorderFactory.createCompoundBorder( BorderFactory.createMatteBorder(0, 0, 0, 1, TrackConstants.BORDER_COLOR), BorderFactory.createEmptyBorder(2, 2, 2, 2))); header.setBackground(Color.LIGHT_GRAY); // Normally I would use pushx instead of defining the width, but in this // case I defined the width because span combined with push makes the // first action icon cell push out as well. 136 was calculated from // 140 pixels minus 2 minus 2 (from the empty border defined above). header.add(trackLabel, "span 6, w 136!, center, growx"); header.add(iconLabel, "span 6, w 136!, h 32!, center, growx"); // Set up the button used for locking/unlocking track movement { lockUnlockButton = new JButton(TrackConstants.UNLOCK_ICON); lockUnlockButton.setName("lockUnlockButton"); lockUnlockButton.setContentAreaFilled(false); lockUnlockButton.setBorderPainted(false); lockUnlockButton.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { handleLockUnlockButtonEvent(e); } }); Map<String, String> constraints = Maps.newHashMap(); constraints.put("width", Integer.toString(TrackConstants.ACTION_BUTTON_WIDTH)); constraints.put("height", Integer.toString(TrackConstants.ACTION_BUTTON_HEIGHT)); String template = "cell 0 2, w ${width}!, h ${height}!"; StrSubstitutor sub = new StrSubstitutor(constraints); header.add(lockUnlockButton, sub.replace(template)); } // Set up the button used for hiding/showing a track's data viewer { visibleButton = new JButton(TrackConstants.VIEWER_HIDE_ICON); visibleButton.setName("visibleButton"); visibleButton.setContentAreaFilled(false); visibleButton.setBorderPainted(false); visibleButton.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { handleVisibleButtonEvent(e); } }); Map<String, String> constraints = Maps.newHashMap(); constraints.put("width", Integer.toString(TrackConstants.ACTION_BUTTON_WIDTH)); constraints.put("height", Integer.toString(TrackConstants.ACTION_BUTTON_HEIGHT)); String template = "cell 1 2, w ${width}!, h ${height}!"; StrSubstitutor sub = new StrSubstitutor(constraints); header.add(visibleButton, sub.replace(template)); } // Set up the button used for removing a track and its plugin { rubbishButton = new JButton(TrackConstants.DELETE_ICON); rubbishButton.setName("rubbishButton"); rubbishButton.setContentAreaFilled(false); rubbishButton.setBorderPainted(false); rubbishButton.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { handleDeleteButtonEvent(e); } }); Map<String, String> constraints = Maps.newHashMap(); constraints.put("width", Integer.toString(TrackConstants.ACTION_BUTTON_WIDTH)); constraints.put("height", Integer.toString(TrackConstants.ACTION_BUTTON_HEIGHT)); String template = "cell 5 2, w ${width}!, h ${height}!"; StrSubstitutor sub = new StrSubstitutor(constraints); header.add(rubbishButton, sub.replace(template)); } // Add the header to our layout. { Map<String, String> constraints = Maps.newHashMap(); constraints.put("width", Integer.toString(TrackConstants.HEADER_WIDTH)); constraints.put("height", Integer.toString(TrackConstants.CARRIAGE_HEIGHT)); String template = "w ${width}!, h ${height}!"; StrSubstitutor sub = new StrSubstitutor(constraints); view.add(header, sub.replace(template)); } // Add the track carriage to our layout. { Map<String, String> constraints = Maps.newHashMap(); constraints.put("height", Integer.toString(TrackConstants.CARRIAGE_HEIGHT)); String template = "pushx, growx, h ${height}!"; StrSubstitutor sub = new StrSubstitutor(constraints); view.add(trackPainter, sub.replace(template)); } view.validate(); } private void updatePopupMenu() { // 1. Remove every other popup menu item apart from the defaults. menu.removeAll(); menu.add(setBookmarkMenuItem); menu.add(clearBookmarkMenuItem); // 2. Add a divider if there are bookmarks. if (!trackModel.getBookmarks().isEmpty()) { menu.addSeparator(); } // 3. Add menu item for deleting individual bookmarks for (final Long bookmark : trackModel.getBookmarks()) { String text = DataControllerV.formatTime(bookmark); JMenuItem delete = new JMenuItem("Delete bookmark " + text); delete.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { removeBookmark(bookmark); } }); menu.add(delete); } } /** * Sets the track information to use. * *@param id * Identifier to use. * @param icon * Icon to use with this track. {@code null} if no icon. * @param trackName * Name of this track * @param trackPath * Absolute path to the track's data feed * @param duration * Duration of the data feed in milliseconds * @param offset * Offset of the data feed in milliseconds */ public void setTrackInformation(final Identifier id, final ImageIcon icon, final String trackName, final String trackPath, final long duration, final long offset) { if (icon != null) { iconLabel.setIcon(icon); } trackModel.setId(id); trackModel.setTrackName(trackName); trackModel.setMediaPath(trackPath); trackModel.setDuration(duration); trackModel.setOffset(offset); trackModel.setErroneous(false); trackLabel.setText(trackName); trackLabel.setToolTipText(trackName); trackPainter.setTrackModel(trackModel); } /** * Sets the track offset in milliseconds. * * @param offset * Offset of the data feed in milliseconds */ public void setTrackOffset(final long offset) { trackModel.setOffset(offset); trackPainter.setTrackModel(trackModel); } private ImageIcon getVisibleButtonIcon() { if (isViewerVisible) { return TrackConstants.VIEWER_HIDE_ICON; } else { return TrackConstants.VIEWER_SHOW_ICON; } } /** * Indicate that the track's information cannot be resolved. * * @param erroneous true if the data is erroneous, false otherwise. */ public void setErroneous(final boolean erroneous) { trackModel.setErroneous(erroneous); trackPainter.setTrackModel(trackModel); } /** * Add a bookmark location to the track. Does not take track offsets into * account. * * @param bookmark * bookmark position in milliseconds */ public void addBookmark(final long bookmark) { if ((0 <= bookmark) && (bookmark <= trackModel.getDuration())) { trackModel.addBookmark(bookmark); trackPainter.setTrackModel(trackModel); } updatePopupMenu(); } public void addBookmarks(final List<Long> bookmarks) { trackModel.addBookmarks(bookmarks); trackPainter.setTrackModel(trackModel); updatePopupMenu(); } public void removeBookmark(final long bookmark) { trackModel.removeBookmark(bookmark); trackPainter.setTrackModel(trackModel); updatePopupMenu(); } /** * Add a bookmark location to the track. Track offsets are taken into * account. This call is the same as addBookmark(position - offset). * * @param position * temporal position in milliseconds to bookmark. */ public void addTemporalBookmark(final long position) { addBookmark(position - trackModel.getOffset()); } /** * Sets the state of the track model. * * @param state the new state to set. */ private void setState(final TrackState state) { trackModel.setState(state); trackPainter.setTrackModel(trackModel); } /** * @return True if the track is selected, false otherwise. */ public boolean isSelected() { return trackModel.isSelected(); } /** * @return True if the track is locked, false otherwise. */ public boolean isLocked() { return trackModel.isLocked(); } /** * @return Offset in milliseconds. */ public long getOffset() { return trackModel.getOffset(); } /** * @return Returns the duration of the track in milliseconds. Does not take * into account any offsets. */ public long getDuration() { return trackModel.getDuration(); } /** * @return Bookmarked positions in milliseconds. Does not take into account * any offsets. */ public List<Long> getBookmarks() { return trackModel.getBookmarks(); } /** * @return track name, i.e. file name. */ public String getTrackName() { return trackLabel.getText(); } /** * @return View used by the controller */ public JComponent getView() { return view; } /** * @return a clone of the track model used by the controller */ public TrackModel getTrackModel() { return trackModel.copy(); } /** * Set if the track carriage can be moved while the snap functionality is * switched on. * * @param canMove true if the carriage can be moved, false otherwise. */ public void setMoveable(final boolean canMove) { isMoveable = canMove; } /** * Set if the track carriage can be moved. * * @param lock true if the carriage is locked, false otherwise. */ public void setLocked(final boolean lock) { trackModel.setLocked(lock); if (lock) { lockUnlockButton.setIcon(TrackConstants.LOCK_ICON); } else { lockUnlockButton.setIcon(TrackConstants.UNLOCK_ICON); } } /** * Used to request bookmark saving. */ public void saveBookmark() { fireCarriageBookmarkSaveEvent(); } public void deselect() { trackModel.setSelected(false); trackPainter.setTrackModel(trackModel); } /** * When the viewer tells us that the state of the project should change, * tell Datavyu to update the projectChanged state. */ @Override public void notifyStateChanged(final String propertyChanged, final String newValue) { if (propertyChanged != null) { // Determine if we can handle the requested change boolean handled = false; String property = propertyChanged.toLowerCase(); // FIXME empty property names are not allowed because we can't // create nameless variables. null properties are allowed but used // to represent multiple property changes. if (property.equals("")) { handled = true; } // FIXME this is hackish and it couples the plugins directly to // how this method works. Maybe a new data controller interface // method is needed. No other plugin developers are aware that // we can change the duration of a media file. if (property.equals("duration")) { handled = true; Long val = null; try { val = Long.parseLong(newValue); } catch (NumberFormatException ex) { LOGGER.error("Error in format of long value: " + newValue); handled = false; } if (val != null) { trackModel.setDuration(val); view.repaint(); Datavyu.getDataController().updateMaxViewerDuration(); Datavyu.getDataController().getMixerController().clearRegionAndZoomOut(); } } // FIXME this is not an error, property change listeners should just // ignore properties they are not interested in. if (!handled) { // We couldn't find a way to handle the change- report this. LOGGER.error( "Unhandled property change: notified update of " + propertyChanged + " to " + newValue); } } // FIXME move interface method into project controller? Datavyu.getProjectController().projectChanged(); } public void bindTrackActions(final CustomActions actions) { Runnable edtTask = new Runnable() { @Override public void run() { Map<String, String> constraints = Maps.newHashMap(); constraints.put("width", Integer.toString(TrackConstants.ACTION_BUTTON_WIDTH)); constraints.put("height", Integer.toString(TrackConstants.ACTION_BUTTON_HEIGHT)); String template = "w ${width}!, h ${height}!"; StrSubstitutor sub = new StrSubstitutor(constraints); String cons = sub.replace(template); if (actions.getActionButton1() != null) { header.add(actions.getActionButton1(), cons + ", cell 2 2"); } if (actions.getActionButton2() != null) { header.add(actions.getActionButton2(), cons + ", cell 3 2"); } if (actions.getActionButton3() != null) { header.add(actions.getActionButton3(), cons + ", cell 4 2"); } header.validate(); } }; if (SwingUtilities.isEventDispatchThread()) { edtTask.run(); } else { SwingUtilities.invokeLater(edtTask); } } /** * Request a bookmark. */ private void setBookmarkAction() { fireCarriageBookmarkRequestEvent(); } /** * Remove the track's bookmark. */ private void clearBookmarkAction() { trackModel.clearBookmarks(); trackPainter.setTrackModel(trackModel); } /** * Deselects the track if it is selected. */ private void deselectTrack() { if (trackModel.isSelected()) { trackModel.setSelected(false); trackPainter.setTrackModel(trackModel); fireCarriageSelectionChangeEvent(false); } } /** * Selects the track if it isn't already selected. */ private void selectTrack() { if (!trackModel.isSelected()) { trackModel.setSelected(true); trackPainter.setTrackModel(trackModel); fireCarriageSelectionChangeEvent(false); } } /** * Handles the event for locking and unlocking the track's movement. * * @param e event to handle. */ private void handleLockUnlockButtonEvent(final ActionEvent e) { boolean isLocked = trackModel.isLocked(); isLocked ^= true; trackModel.setLocked(isLocked); setLocked(isLocked); fireLockStateChangedEvent(); } /** * Handles the event for removing a track with the rubbish bin button. * @param e The event to handle. */ private void handleDeleteButtonEvent(final ActionEvent e) { Datavyu.getDataController().shutdown(trackModel.getId()); } /** * Handles the event for hiding/showing a data viewer with the eye button. * @param e The event to handle. */ private void handleVisibleButtonEvent(final ActionEvent e) { isViewerVisible = !isViewerVisible; Datavyu.getDataController().setDataViewerVisibility(trackModel.getId(), isViewerVisible); visibleButton.setIcon(getVisibleButtonIcon()); } /** * Register a mouse listener. * * @param listener listener to register. */ public void addMouseListener(final MouseListener listener) { synchronized (this) { view.addMouseListener(listener); } } /** * Remove the mouse listener. * * @param listener listener to remove. */ public void removeMouseListener(final MouseListener listener) { synchronized (this) { view.removeMouseListener(listener); } } /** * Register the listener to be notified of carriage events. * * @param listener listener to register. */ public void addCarriageEventListener(final CarriageEventListener listener) { synchronized (this) { listenerList.add(CarriageEventListener.class, listener); } } /** * Remove the listener from being notified of carriage events. * * @param listener listener to remove. */ public void removeCarriageEventListener(final CarriageEventListener listener) { synchronized (this) { listenerList.remove(CarriageEventListener.class, listener); } } /** * Register the listener interested in mouse events on the track's carriage. * * @param listener listener to register. */ public void addTrackMouseEventListener(final TrackMouseEventListener listener) { synchronized (this) { listenerList.add(TrackMouseEventListener.class, listener); } } /** * Remove the listener from being notified of mouse events on the track's * carriage. * * @param listener listener to remove. */ public void removeTrackMouseEventListener(final TrackMouseEventListener listener) { synchronized (this) { listenerList.remove(TrackMouseEventListener.class, listener); } } /** * Used to inform listeners about a new carriage event. * * @param newOffset the new offset to inform listeners about. * @param temporalPosition * the temporal position of the mouse when the new offset is * triggered * @param hasModifiers true if modifiers were held down, false otherwise. */ private void fireCarriageOffsetChangeEvent(final long newOffset, final long temporalPosition, final boolean hasModifiers) { synchronized (this) { final CarriageEvent e = new CarriageEvent(this, trackModel.getId(), newOffset, trackModel.getBookmarks(), trackModel.getDuration(), temporalPosition, EventType.OFFSET_CHANGE, hasModifiers); final Object[] listeners = listenerList.getListenerList(); /* * The listener list contains the listening class and then the * listener instance. */ for (int i = 0; i < listeners.length; i += 2) { if (listeners[i] == CarriageEventListener.class) { ((CarriageEventListener) listeners[i + 1]).offsetChanged(e); } } } } /** * Used to inform listeners about a bookmark request event. */ private void fireCarriageBookmarkRequestEvent() { synchronized (this) { final CarriageEvent e = new CarriageEvent(this, trackModel.getId(), trackModel.getOffset(), trackModel.getBookmarks(), trackModel.getDuration(), 0, EventType.BOOKMARK_REQUEST, false); final Object[] listeners = listenerList.getListenerList(); /* * The listener list contains the listening class and then the * listener instance. */ for (int i = 0; i < listeners.length; i += 2) { if (listeners[i] == CarriageEventListener.class) { ((CarriageEventListener) listeners[i + 1]).requestBookmark(e); } } } } /** * Used to inform listeners about a bookmark request event. */ private void fireCarriageBookmarkSaveEvent() { synchronized (this) { final CarriageEvent e = new CarriageEvent(this, trackModel.getId(), trackModel.getOffset(), trackModel.getBookmarks(), trackModel.getDuration(), 0, EventType.BOOKMARK_SAVE, false); final Object[] listeners = listenerList.getListenerList(); /* * The listener list contains the listening class and then the * listener instance. */ for (int i = 0; i < listeners.length; i += 2) { if (listeners[i] == CarriageEventListener.class) { ((CarriageEventListener) listeners[i + 1]).saveBookmark(e); } } } } /** * Used to inform listeners about track selection event. * * @param hasModifiers true if modifiers were held down, false otherwise. */ private void fireCarriageSelectionChangeEvent(final boolean hasModifiers) { synchronized (this) { final CarriageEvent e = new CarriageEvent(this, trackModel.getId(), trackModel.getOffset(), trackModel.getBookmarks(), trackModel.getDuration(), 0, EventType.CARRIAGE_SELECTION, hasModifiers); final Object[] listeners = listenerList.getListenerList(); /* * The listener list contains the listening class and then the * listener instance. */ for (int i = 0; i < listeners.length; i += 2) { if (listeners[i] == CarriageEventListener.class) { ((CarriageEventListener) listeners[i + 1]).selectionChanged(e); } } } } /** * Used to inform listeners about lock state change event. */ private void fireLockStateChangedEvent() { synchronized (this) { final CarriageEvent e = new CarriageEvent(this, trackModel.getId(), trackModel.getOffset(), trackModel.getBookmarks(), trackModel.getDuration(), 0, EventType.CARRIAGE_LOCK, false); final Object[] listeners = listenerList.getListenerList(); /* * The listener list contains the listening class and then the * listener instance. */ for (int i = 0; i < listeners.length; i += 2) { if (listeners[i] == CarriageEventListener.class) { ((CarriageEventListener) listeners[i + 1]).lockStateChanged(e); } } } } /** * Used to inform listeners about the mouse release event on the track's * carriage. * * @param e the event to handle. */ private void fireMouseReleasedEvent(final MouseEvent e) { synchronized (this) { final Object[] listeners = listenerList.getListenerList(); /* * The listener list contains the listening class and then the * listener instance. */ for (int i = 0; i < listeners.length; i += 2) { if (listeners[i] == TrackMouseEventListener.class) { ((TrackMouseEventListener) listeners[i + 1]).mouseReleased(e); } } } } @Override public void propertyChange(final PropertyChangeEvent evt) { if (evt.getSource() == mixerModel.getViewportModel()) { view.repaint(); } } public void attachAsWindowListener() { Datavyu.getDataController().bindWindowListenerToDataViewer(trackModel.getId(), new WindowAdapter() { @Override public void windowClosing(final WindowEvent e) { isViewerVisible = false; visibleButton.setIcon(getVisibleButtonIcon()); } }); } /** * Calculates the time threshold below which data tracks will snap into place. * * @param viewport current viewport * @return snapping threshold in time units (milliseconds); */ public static long calculateSnappingThreshold(final ViewportState viewport) { final long MINIMUM_THRESHOLD_MILLISECONDS = 10; return Math.max((long) Math.ceil(0.01F * viewport.getViewDuration()), MINIMUM_THRESHOLD_MILLISECONDS); } /** * Inner listener used to handle mouse events. */ private class TrackPainterListener extends MouseInputAdapter { /** Initial offset value. */ private long offsetInit; /** Is the mouse in the carriage. */ private boolean inCarriage; /** Whether the track was selected when the mouse was first pressed. */ private boolean wasTrackSelected; /** Initial x-coord position. */ private int xInit; /** Initial track state. */ private TrackState initialState; /** Mouse cursor when hovering over a track that can be moved. */ private final Cursor moveableTrackHoverCursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); /** Default mouse cursor. */ private final Cursor defaultCursor = Cursor.getDefaultCursor(); private ViewportState viewport; @Override public void mouseEntered(final MouseEvent e) { updateCursor(e); } @Override public void mouseClicked(final MouseEvent e) { if (trackPainter.getCarriagePolygon().contains(e.getPoint()) && wasTrackSelected) { deselectTrack(); } } @Override public void mouseMoved(final MouseEvent e) { updateCursor(e); } private void updateCursor(final MouseEvent e) { final boolean isHovering = trackPainter.getCarriagePolygon().contains(e.getPoint()); trackPainter .setCursor((!trackModel.isLocked() && isHovering) ? moveableTrackHoverCursor : defaultCursor); } @Override public void mousePressed(final MouseEvent e) { viewport = mixerModel.getViewportModel().getViewport(); wasTrackSelected = trackModel.isSelected(); if (trackPainter.getCarriagePolygon().contains(e.getPoint())) { inCarriage = true; xInit = e.getX(); offsetInit = trackModel.getOffset(); selectTrack(); initialState = trackModel.getState(); handleOffsetChanges(e); } if (e.isPopupTrigger()) { menu.show(e.getComponent(), e.getX(), e.getY()); } } @Override public void mouseDragged(final MouseEvent e) { if (trackModel.isLocked()) { return; } handleOffsetChanges(e); } private void handleOffsetChanges(final MouseEvent e) { final boolean hasModifiers = e.isAltDown() || e.isAltGraphDown() || e.isControlDown() || e.isMetaDown() || e.isShiftDown(); if (inCarriage) { final int xNet = e.getX() - xInit; final double newOffset = viewport.computeTimeFromXOffset(xNet) + offsetInit; final long temporalPosition = viewport.computeTimeFromXOffset(e.getX()) + viewport.getViewStart(); if (isMoveable) { fireCarriageOffsetChangeEvent((long) newOffset, temporalPosition, hasModifiers); } else { final long threshold = calculateSnappingThreshold(viewport); if (Math.abs(newOffset - offsetInit) >= threshold) { isMoveable = true; } } } } @Override public void mouseReleased(final MouseEvent e) { isMoveable = true; inCarriage = false; final Component source = (Component) e.getSource(); source.setCursor(defaultCursor); setState(initialState); if (e.isPopupTrigger()) { menu.show(e.getComponent(), e.getX(), e.getY()); } fireMouseReleasedEvent(e); } } }