/******************************************************************************* * SDR Trunk * Copyright (C) 2014,2015 Dennis Sheirer * * 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 ******************************************************************************/ package spectrum; import java.awt.Component; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Hashtable; import javax.swing.JCheckBoxMenuItem; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JLayeredPane; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JSeparator; import javax.swing.JSlider; import javax.swing.JSplitPane; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.MouseInputAdapter; import module.decode.DecoderType; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import playlist.PlaylistManager; import properties.SystemProperties; import sample.Listener; import sample.SampleType; import sample.complex.ComplexBuffer; import settings.ColorSetting.ColorSettingName; import settings.ColorSettingMenuItem; import settings.SettingsManager; import source.SourceException; import source.tuner.Tuner; import source.tuner.TunerSelectionListener; import source.tuner.frequency.FrequencyChangeEvent; import source.tuner.frequency.FrequencyChangeEvent.Event; import source.tuner.frequency.FrequencyChangeListener; import spectrum.OverlayPanel.ChannelDisplay; import spectrum.converter.ComplexDecibelConverter; import spectrum.converter.DFTResultsConverter; import spectrum.menu.AveragingItem; import spectrum.menu.DFTSizeItem; import spectrum.menu.FFTWindowTypeItem; import spectrum.menu.FrameRateItem; import spectrum.menu.SmoothingItem; import spectrum.menu.SmoothingTypeItem; import com.jidesoft.swing.JideSplitPane; import controller.ConfigurationControllerModel; import controller.channel.Channel; import controller.channel.ChannelManager; import controller.channel.ChannelUtils; import dsp.filter.Window.WindowType; import dsp.filter.smoothing.SmoothingFilter.SmoothingType; public class SpectralDisplayPanel extends JPanel implements Listener, FrequencyChangeListener, IDFTWidthChangeProcessor, TunerSelectionListener { private static final long serialVersionUID = 1L; private final static Logger mLog = LoggerFactory.getLogger( SpectralDisplayPanel.class ); private static DecimalFormat sCURSOR_FORMAT = new DecimalFormat( "000.00000" ); public static final String FFT_SIZE_PROPERTY = "spectral.display.dft.size"; public static final int NO_ZOOM = 0; public static final int MAX_ZOOM = 6; private DFTSize mDFTSize = DFTSize.FFT04096; private int mZoom = 0; private int mDFTZoomWindowOffset = 0; private JScrollPane mScrollPane; private JLayeredPane mLayeredPanel; private SpectrumPanel mSpectrumPanel; private WaterfallPanel mWaterfallPanel; private OverlayPanel mOverlayPanel; private DFTProcessor mDFTProcessor; private DFTResultsConverter mDFTConverter; private PlaylistManager mPlaylistManager; private SettingsManager mSettingsManager; private ConfigurationControllerModel mController; private Tuner mTuner; /** * Spectral Display Panel provides a frequency component display with a * historical waterfall display and a transparent overlay to show frequency, * cursor and channel information. * * Mouse scrolling and zooming are supported and the waterfall display can * be paused. * * Complex sample buffers are processed by a DFTProcessor and the output of * the DFT is translated to decibels for display in the spectrum and * waterfall components. */ public SpectralDisplayPanel( ChannelManager channelManager, ConfigurationControllerModel controller, PlaylistManager playlistManager, SettingsManager settingsManager ) { mController = controller; mPlaylistManager = playlistManager; mSettingsManager = settingsManager; mSpectrumPanel = new SpectrumPanel( mSettingsManager ); mOverlayPanel = new OverlayPanel( mSettingsManager, channelManager ); mWaterfallPanel = new WaterfallPanel( mSettingsManager ); //Register to receive tuner selection events if( mController != null ) { mController.addListener( (TunerSelectionListener)this ); } init(); loadSettings(); } private void loadSettings() { SystemProperties properties = SystemProperties.getInstance(); String rawSize = properties.get( FFT_SIZE_PROPERTY, DFTSize.FFT04096.name() ); DFTSize size = null; if( rawSize != null ) { try { size = DFTSize.valueOf( rawSize ); } catch( Exception e ) { //Do nothing, we couldn't parse the stored value } } if( size == null ) { size = DFTSize.FFT04096; } setDFTSize( size ); } public void dispose() { /* De-register from receiving samples when the window closes */ clearTuner(); if( mController != null ) { mController.removeListener( (TunerSelectionListener)this ); } mController = null; mPlaylistManager = null; mSettingsManager = null; mDFTProcessor.dispose(); mDFTProcessor = null; mDFTConverter.dispose(); mDFTConverter = null; mSpectrumPanel.dispose(); mSpectrumPanel = null; mWaterfallPanel.dispose(); mWaterfallPanel = null; mOverlayPanel.dispose(); mOverlayPanel = null; mTuner = null; } /** * Queues an FFT size change request. The scheduled executor will apply * the change when it runs. */ public void setDFTSize( DFTSize size ) { mDFTProcessor.setDFTSize( size ); mOverlayPanel.setDFTSize( size ); mDFTSize = size; SystemProperties properties = SystemProperties.getInstance(); properties.set( FFT_SIZE_PROPERTY, size.name() ); properties.save(); setZoom( 0, 0, 0 ); } @Override public DFTSize getDFTSize() { return mDFTSize; } public int getZoom() { return mZoom; } /** * Sets the current zoom level which will be 2 to the power of zoom (2^zoom) * * 0 No Zoom * 1 2x Zoom * 2 4x Zoom * 3 8x Zoom * 4 16x Zoom * 5 32x Zoom * 6 64x Zoom * * @param zoom level, 0 - 6. * @param frequency under the mouse to maintain while zooming * @param xAxisOffset where to maintain the frequency under the mouse */ public void setZoom( int zoom, long frequency, double windowOffset ) { if( zoom < NO_ZOOM ) { zoom = NO_ZOOM; } else if( zoom > MAX_ZOOM ) { zoom = MAX_ZOOM; } if( zoom != mZoom ) { mZoom = zoom; //Calculate the bin offset that would place the reference frequency //at the left edge of the zoom window. double binOffsetToFrequency = getBinOffset( frequency ); //Calculate the bin offset into the newly sized zoom window that //would place the frequency in the same proportional window location //that it was in the previous zoom size double windowBinOffset = (double)getZoomWindowSizeInBins() * windowOffset; //Set the overall offset to place the reference frequency in the //same location in the newly zoomed window double offset = binOffsetToFrequency - windowBinOffset; mSpectrumPanel.setZoom( mZoom ); mOverlayPanel.setZoom( mZoom ); mWaterfallPanel.setZoom( mZoom ); setZoomWindowOffset( offset ); } } /** * Sets the offset (in DFT bins) to the first bin that the zoom window displays */ public void setZoomWindowOffset( double offset ) { if( offset < 0 ) { offset = 0; } if( offset > ( mDFTSize.getSize() - getZoomWindowSizeInBins() ) ) { offset = mDFTSize.getSize() - getZoomWindowSizeInBins(); } mDFTZoomWindowOffset = (int)offset; mSpectrumPanel.setZoomWindowOffset( mDFTZoomWindowOffset ); mOverlayPanel.setZoomWindowOffset( mDFTZoomWindowOffset ); mWaterfallPanel.setZoomWindowOffset( mDFTZoomWindowOffset ); } /** * Calculates the size of the current zoom window in DFT bins */ private int getZoomWindowSizeInBins() { return mDFTSize.getSize() / getZoomMultiplier(); } public int getZoomMultiplier() { return (int)Math.pow( 2.0, mZoom ); } /** * Calculates the overall offset of the frequency from the current minimum * frequency in terms of total FFT width * @param frequency * @return */ private double getBinOffset( long frequency ) { double offset = 0.0; if( mOverlayPanel.containsFrequency( frequency ) ) { offset = (double)mDFTSize.getSize() * ( (double)( frequency - mOverlayPanel.getMinFrequency() ) / (double)mOverlayPanel.getBandwidth() ); } return offset; } /** * Overrides JComponent method to return false, since we have overlapping * panels with the spectrum and channel panels */ public boolean isOptimizedDrawingEnabled() { return false; } private void init() { setLayout( new MigLayout( "insets 0 0 0 0", "[grow]", "[grow]") ); /** * The layered pane holds the overlapping spectrum and channel panels * and manages the sizing of each panel with the resize listener */ mLayeredPanel = new JLayeredPane(); mLayeredPanel.addComponentListener( new ResizeListener() ); /** * Create a mouse adapter to handle mouse events over the spectrum * and waterfall panels */ MouseEventProcessor mouser = new MouseEventProcessor(); mOverlayPanel.addMouseListener( mouser ); mOverlayPanel.addMouseMotionListener( mouser ); mOverlayPanel.addMouseWheelListener( mouser ); //Add the spectrum and channel panels to the layered panel mLayeredPanel.add( mSpectrumPanel, new Integer( 0 ), 0 ); mLayeredPanel.add( mOverlayPanel, new Integer( 1 ), 0 ); //Create the waterfall mWaterfallPanel.addMouseListener( mouser ); mWaterfallPanel.addMouseMotionListener( mouser ); mWaterfallPanel.addMouseWheelListener( mouser ); /* Attempt to set a 50/50 split preferred size for the split pane */ double totalHeight = mLayeredPanel.getPreferredSize().getHeight() + mWaterfallPanel.getPreferredSize().getHeight(); mLayeredPanel.setPreferredSize( new Dimension( (int)mLayeredPanel .getPreferredSize().getWidth(), (int)( totalHeight / 2.0d ) ) ); mWaterfallPanel.setPreferredSize( new Dimension( (int)mWaterfallPanel .getPreferredSize().getWidth(), (int)( totalHeight / 2.0d ) ) ); //Create the split pane to hold the layered pane and the waterfall JideSplitPane splitPane = new JideSplitPane( JSplitPane.VERTICAL_SPLIT ); splitPane.setDividerSize( 5 ); splitPane.add( mLayeredPanel ); splitPane.add( mWaterfallPanel ); mScrollPane = new JScrollPane( splitPane ); add( mScrollPane, "grow" ); /** * Setup DFTProcessor to process samples and register the waterfall and * spectrum panel to receive the processed dft results */ mDFTProcessor = new DFTProcessor( SampleType.COMPLEX ); mDFTConverter = new ComplexDecibelConverter(); mDFTProcessor.addConverter( mDFTConverter ); mDFTConverter.addListener( (DFTResultsListener)mSpectrumPanel ); mDFTConverter.addListener( (DFTResultsListener)mWaterfallPanel ); } /** * Receives frequency change events -- primarily from tuner components. */ @Override public void frequencyChanged( FrequencyChangeEvent event ) { mOverlayPanel.frequencyChanged( event ); mDFTProcessor.frequencyChanged( event ); } /** * Complex sample buffer receive method */ @Override public void receive( ComplexBuffer sampleBuffer ) { mDFTProcessor.receive( sampleBuffer ); } /** * Responds to tuner selection event by deregistering from the current * complex sample buffer source and registering with the tuner argument. */ @Override public void tunerSelected( Tuner tuner ) { clearTuner(); mDFTProcessor.clearBuffer(); mTuner = tuner; if( mTuner != null ) { //Register to receive frequency change events mTuner.addListener( (FrequencyChangeListener)this ); //Register the dft processor to receive samples from the tuner mTuner.addListener( (Listener)mDFTProcessor ); mSpectrumPanel.setSampleSize( mTuner.getSampleSize() ); //Fire frequency and sample rate change events so that the spectrum //and overlay panels can synchronize try { frequencyChanged( new FrequencyChangeEvent( Event.FREQUENCY_CHANGE_NOTIFICATION, mTuner.getFrequency() ) ); frequencyChanged( new FrequencyChangeEvent( Event.SAMPLE_RATE_CHANGE_NOTIFICATION, mTuner.getSampleRate() ) ); } catch ( SourceException e ) { mLog.info( "DFTProcessor - exception during new tuner setup - " + "couldn't get frequency from the tuner", e ); } } } /** * Tuner de-selection cleanup method */ public void clearTuner() { if( mTuner != null ) { //Deregister for frequency change events from the tuner mTuner.removeListener( (FrequencyChangeListener)this ); //Deregister the dft processor from receiving samples mTuner.removeListener( (Listener)mDFTProcessor ); mTuner = null; } } /** * Monitors the sizing of the layered pane and resizes the spectrum and * channel panels whenever the layered pane is resized */ public class ResizeListener implements ComponentListener { @Override public void componentResized( ComponentEvent e ) { Component c = e.getComponent(); mSpectrumPanel.setBounds( 0, 0, c.getWidth(), c.getHeight() ); mOverlayPanel.setBounds( 0, 0, c.getWidth(), c.getHeight() ); } @Override public void componentHidden( ComponentEvent arg0 ) {} @Override public void componentMoved( ComponentEvent arg0 ) {} @Override public void componentShown( ComponentEvent arg0 ) {} } /** * Mouse event handler for the spectral display panel. */ public class MouseEventProcessor extends MouseInputAdapter { private int mDFTZoomWindowOffsetAtDragStart = 0; private int mDragStartX = 0; private double mPixelsPerBin; public MouseEventProcessor() { } @Override public void mouseWheelMoved( MouseWheelEvent e ) { int zoom = mZoom - e.getWheelRotation(); long frequency = mOverlayPanel.getFrequencyFromAxis( e.getX() ); double windowOffset = (double)e.getX() / (double)getWidth(); setZoom( zoom, frequency, windowOffset ); } @Override public void mouseMoved( MouseEvent event ) { update( event ); } @Override public void mouseDragged( MouseEvent event ) { update( event ); int dragDistance = mDragStartX - event.getX(); double binDistance = (double)dragDistance / mPixelsPerBin; int offset = (int)( mDFTZoomWindowOffsetAtDragStart + binDistance ); if( offset < 0 ) { offset = 0; } int maxOffset = mDFTSize.getSize() - ( mDFTSize.getSize() / getZoomMultiplier() ); if( offset > maxOffset ) { offset = maxOffset; } setZoomWindowOffset( offset ); } @Override public void mousePressed( MouseEvent e ) { mDragStartX = e.getX(); mDFTZoomWindowOffsetAtDragStart = mDFTZoomWindowOffset; mPixelsPerBin = (double)getWidth() / ( (double)( mDFTSize.getSize() ) / (double)getZoomMultiplier() ); } /** * Updates the cursor display while the mouse is performing actions */ private void update( MouseEvent event ) { if( event.getComponent() == mOverlayPanel ) { mOverlayPanel.setCursorLocation( event.getPoint() ); } else { mWaterfallPanel.setCursorLocation( event.getPoint() ); mWaterfallPanel.setCursorFrequency( mOverlayPanel.getFrequencyFromAxis( event.getPoint().x ) ); } } @Override public void mouseEntered( MouseEvent e ) { if( e.getComponent() == mOverlayPanel ) { mOverlayPanel.setCursorVisible( true ); } else { mWaterfallPanel.setCursorVisible( true ); } } @Override public void mouseExited( MouseEvent e ) { mOverlayPanel.setCursorVisible( false ); mWaterfallPanel.setCursorVisible( false ); } /** * Displays the context menu. */ @Override public void mouseClicked( MouseEvent event ) { if( SwingUtilities.isRightMouseButton( event ) ) { JPopupMenu contextMenu = new JPopupMenu(); if( event.getComponent() == mWaterfallPanel ) { contextMenu.add( new PauseItem( mWaterfallPanel, "Pause" ) ); } long frequency = mOverlayPanel.getFrequencyFromAxis( event.getX() ); if( event.getComponent() == mOverlayPanel ) { ArrayList channels = mOverlayPanel.getChannelsAtFrequency( frequency ); for( Channel channel: channels ) { JMenu channelMenu = ChannelUtils.getContextMenu( mPlaylistManager, channel, SpectralDisplayPanel.this ); if( channelMenu != null ) { contextMenu.add( channelMenu ); } } if( !channels.isEmpty() ) { contextMenu.add( new JSeparator() ); } } JMenu frequencyMenu = new JMenu( sCURSOR_FORMAT.format( (float)frequency / 1000000.0f ) ); JMenu decoderMenu = new JMenu( "Add Decoder" ); for( DecoderType type: DecoderType.getPrimaryDecoders() ) { decoderMenu.add( new DecoderItem( mController, frequency, type ) ); } frequencyMenu.add( decoderMenu ); contextMenu.add( frequencyMenu ); contextMenu.add( new JSeparator() ); /** * Color Menus */ JMenu colorMenu = new JMenu( "Color" ); colorMenu.add( new ColorSettingMenuItem( mSettingsManager, ColorSettingName.CHANNEL_CONFIG ) ); colorMenu.add( new ColorSettingMenuItem( mSettingsManager, ColorSettingName.CHANNEL_CONFIG_PROCESSING ) ); colorMenu.add( new ColorSettingMenuItem( mSettingsManager, ColorSettingName.CHANNEL_CONFIG_SELECTED ) ); colorMenu.add( new ColorSettingMenuItem( mSettingsManager, ColorSettingName.SPECTRUM_CURSOR ) ); colorMenu.add( new ColorSettingMenuItem( mSettingsManager, ColorSettingName.SPECTRUM_LINE ) ); colorMenu.add( new ColorSettingMenuItem( mSettingsManager, ColorSettingName.SPECTRUM_BACKGROUND ) ); colorMenu.add( new ColorSettingMenuItem( mSettingsManager, ColorSettingName.SPECTRUM_GRADIENT_BOTTOM ) ); colorMenu.add( new ColorSettingMenuItem( mSettingsManager, ColorSettingName.SPECTRUM_GRADIENT_TOP ) ); contextMenu.add( colorMenu ); /** * Display items: fft and frame rate */ JMenu displayMenu = new JMenu( "Display" ); contextMenu.add( displayMenu ); if( event.getComponent() != mWaterfallPanel ) { /** * Averaging menu */ JMenu averagingMenu = new JMenu( "Averaging" ); averagingMenu.add( new AveragingItem( mSpectrumPanel, 4 ) ); displayMenu.add( averagingMenu ); /** * Channel Display setting menu */ JMenu channelDisplayMenu = new JMenu( "Channel" ); channelDisplayMenu.add( new ChannelDisplayItem( mOverlayPanel, ChannelDisplay.ALL ) ); channelDisplayMenu.add( new ChannelDisplayItem( mOverlayPanel, ChannelDisplay.ENABLED ) ); channelDisplayMenu.add( new ChannelDisplayItem( mOverlayPanel, ChannelDisplay.NONE ) ); displayMenu.add( channelDisplayMenu ); } /** * FFT width */ JMenu fftWidthMenu = new JMenu( "FFT Width" ); displayMenu.add( fftWidthMenu ); for( DFTSize width: DFTSize.values() ) { fftWidthMenu.add( new DFTSizeItem( SpectralDisplayPanel.this, width ) ); } /** * DFT Processor Frame Rate */ JMenu frameRateMenu = new JMenu( "Frame Rate" ); displayMenu.add( frameRateMenu ); frameRateMenu.add( new FrameRateItem( mDFTProcessor, 14 ) ); frameRateMenu.add( new FrameRateItem( mDFTProcessor, 16 ) ); frameRateMenu.add( new FrameRateItem( mDFTProcessor, 18 ) ); frameRateMenu.add( new FrameRateItem( mDFTProcessor, 20 ) ); frameRateMenu.add( new FrameRateItem( mDFTProcessor, 25 ) ); frameRateMenu.add( new FrameRateItem( mDFTProcessor, 30 ) ); frameRateMenu.add( new FrameRateItem( mDFTProcessor, 40 ) ); frameRateMenu.add( new FrameRateItem( mDFTProcessor, 50 ) ); /** * FFT Window Type */ JMenu fftWindowType = new JMenu( "Window Type" ); displayMenu.add( fftWindowType ); for( WindowType type: WindowType.values() ) { fftWindowType.add( new FFTWindowTypeItem( mDFTProcessor, type ) ); } if( event.getComponent() != mWaterfallPanel ) { /** * Smoothing menu */ JMenu smoothingMenu = new JMenu( "Smoothing" ); if( mSpectrumPanel.getSmoothingType() != SmoothingType.NONE ) { smoothingMenu.add( new SmoothingItem( mSpectrumPanel, 5 ) ); smoothingMenu.add( new JSeparator() ); } smoothingMenu.add( new SmoothingTypeItem( mSpectrumPanel, SmoothingType.GAUSSIAN ) ); smoothingMenu.add( new SmoothingTypeItem( mSpectrumPanel, SmoothingType.TRIANGLE ) ); smoothingMenu.add( new SmoothingTypeItem( mSpectrumPanel, SmoothingType.RECTANGLE ) ); smoothingMenu.add( new SmoothingTypeItem( mSpectrumPanel, SmoothingType.NONE ) ); displayMenu.add( smoothingMenu ); } /* * Zoom menu */ JMenuItem zoomMenu = new JMenu( "Zoom" ); double windowOffset = (double)event.getX() / (double)getWidth(); zoomMenu.add( new ZoomItem( frequency, windowOffset ) ); contextMenu.add( zoomMenu ); if( contextMenu != null ) { if( event.getComponent() == mOverlayPanel ) { contextMenu.show( mOverlayPanel, event.getX(), event.getY() ); } else { contextMenu.show( mWaterfallPanel, event.getX(), event.getY() ); } } } } } public class PauseItem extends JCheckBoxMenuItem { private static final long serialVersionUID = 1L; private Pausable mPausable; public PauseItem( Pausable pausable, String label ) { super( label ); final boolean paused = pausable.isPaused(); setSelected( paused ); mPausable = pausable; addActionListener( new ActionListener() { @Override public void actionPerformed( ActionEvent e ) { EventQueue.invokeLater( new Runnable() { @Override public void run() { mPausable.setPaused( !paused ); } } ); } } ); } } public class ZoomItem extends JSlider { private static final long serialVersionUID = 1L; private long mFrequency; private double mWindowOffset; public ZoomItem( long frequency, double windowOffset ) { super( NO_ZOOM, MAX_ZOOM, mZoom ); mFrequency = frequency; mWindowOffset = windowOffset; Hashtable labels = new Hashtable<>(); labels.put( new Integer( 0 ), new JLabel( "1x" ) ); labels.put( new Integer( 1 ), new JLabel( "2x" ) ); labels.put( new Integer( 2 ), new JLabel( "4x" ) ); labels.put( new Integer( 3 ), new JLabel( "8x" ) ); labels.put( new Integer( 4 ), new JLabel( "16x" ) ); labels.put( new Integer( 5 ), new JLabel( "32x" ) ); labels.put( new Integer( 6 ), new JLabel( "64x" ) ); setLabelTable( labels ); setMajorTickSpacing( 1 ); setMinorTickSpacing( 1 ); setPaintTicks( true ); setPaintLabels( true ); this.addChangeListener( new ChangeListener() { @Override public void stateChanged( ChangeEvent e ) { setZoom( getValue(), mFrequency, mWindowOffset ); } } ); } } public class ChannelDisplayItem extends JCheckBoxMenuItem { private static final long serialVersionUID = 1L; private OverlayPanel mOverlayPanel; private ChannelDisplay mChannelDisplay; public ChannelDisplayItem( OverlayPanel panel, ChannelDisplay display ) { super( display.name() ); mOverlayPanel = panel; mChannelDisplay = display; setSelected( mOverlayPanel.getChannelDisplay() == mChannelDisplay ); addActionListener( new ActionListener() { @Override public void actionPerformed( ActionEvent e ) { EventQueue.invokeLater( new Runnable() { @Override public void run() { mOverlayPanel.setChannelDisplay( mChannelDisplay ); } } ); } } ); } } /** * Context menu item to provide one-click access to starting a channel * processing with the selected decoder */ public class DecoderItem extends JMenuItem { private static final long serialVersionUID = 1L; private ConfigurationControllerModel mController; private long mFrequency; private DecoderType mDecoder; public DecoderItem( ConfigurationControllerModel controller, long frequency, DecoderType type ) { super( type.getDisplayString() ); mController = controller; mFrequency = frequency; mDecoder = type; addActionListener( new ActionListener() { @Override public void actionPerformed( ActionEvent e ) { mController.createChannel( mFrequency, mDecoder ); } } ); } } }