/* * ***************************************************************************** * Copyright (C) 2014-2020 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 io.github.dsheirer.channel.metadata; import com.google.common.base.Joiner; import io.github.dsheirer.alias.Alias; import io.github.dsheirer.channel.state.State; import io.github.dsheirer.controller.channel.Channel; import io.github.dsheirer.controller.channel.ChannelModel; import io.github.dsheirer.controller.channel.ChannelProcessingManager; import io.github.dsheirer.eventbus.MyEventBus; import io.github.dsheirer.gui.playlist.channel.ViewChannelRequest; import io.github.dsheirer.icon.IconModel; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier; import io.github.dsheirer.identifier.decoder.ChannelStateIdentifier; import io.github.dsheirer.module.ProcessingChain; import io.github.dsheirer.playlist.PlaylistManager; import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.preference.identifier.TalkgroupFormatPreference; import io.github.dsheirer.preference.swing.JTableColumnWidthMonitor; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.SwingConstants; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.DefaultTableCellRenderer; import java.awt.Color; import java.awt.Component; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.text.DecimalFormat; import java.util.EnumMap; import java.util.List; import java.util.Map; public class ChannelMetadataPanel extends JPanel implements ListSelectionListener { private final static Logger mLog = LoggerFactory.getLogger(ChannelMetadataPanel.class); private static final String TABLE_PREFERENCE_KEY = "channel.metadata.panel"; private ChannelModel mChannelModel; private ChannelProcessingManager mChannelProcessingManager; private IconModel mIconModel; private UserPreferences mUserPreferences; private JTable mTable; private Broadcaster mSelectedProcessingChainBroadcaster = new Broadcaster<>(); private Map mBackgroundColors = new EnumMap<>(State.class); private Map mForegroundColors = new EnumMap<>(State.class); private JTableColumnWidthMonitor mTableColumnMonitor; private Channel mUserSelectedChannel; /** * Table view for currently decoding channel metadata */ public ChannelMetadataPanel(PlaylistManager playlistManager, IconModel iconModel, UserPreferences userPreferences) { mChannelModel = playlistManager.getChannelModel(); mChannelProcessingManager = playlistManager.getChannelProcessingManager(); mIconModel = iconModel; mUserPreferences = userPreferences; init(); } /** * Initializes the panel */ private void init() { setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]") ); mTable = new JTable(mChannelProcessingManager.getChannelMetadataModel()); mChannelProcessingManager.getChannelMetadataModel().setChannelAddListener(new ChannelAddListener()); DefaultTableCellRenderer renderer = (DefaultTableCellRenderer)mTable.getDefaultRenderer(String.class); renderer.setHorizontalAlignment(SwingConstants.CENTER); mTable.getSelectionModel().addListSelectionListener(this); mTable.addMouseListener(new MouseSupport()); mTable.getColumnModel().getColumn(ChannelMetadataModel.COLUMN_DECODER_STATE) .setCellRenderer(new ColoredStateCellRenderer()); mTable.getColumnModel().getColumn(ChannelMetadataModel.COLUMN_USER_FROM) .setCellRenderer(new FromCellRenderer(mUserPreferences.getTalkgroupFormatPreference())); mTable.getColumnModel().getColumn(ChannelMetadataModel.COLUMN_USER_TO) .setCellRenderer(new ToCellRenderer(mUserPreferences.getTalkgroupFormatPreference())); mTable.getColumnModel().getColumn(ChannelMetadataModel.COLUMN_USER_FROM_ALIAS) .setCellRenderer(new AliasCellRenderer()); mTable.getColumnModel().getColumn(ChannelMetadataModel.COLUMN_USER_TO_ALIAS) .setCellRenderer(new AliasCellRenderer()); mTable.getColumnModel().getColumn(ChannelMetadataModel.COLUMN_CONFIGURATION_FREQUENCY) .setCellRenderer(new FrequencyCellRenderer()); //Add a table column width monitor to store/restore column widths mTableColumnMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, TABLE_PREFERENCE_KEY); JScrollPane scrollPane = new JScrollPane(mTable, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); add(scrollPane); setColors(); } /** * Setup the background and foreground color palette for the various channel states. */ private void setColors() { mBackgroundColors.put(State.ACTIVE, Color.CYAN); mForegroundColors.put(State.ACTIVE, Color.BLUE); mBackgroundColors.put(State.CALL, Color.BLUE); mForegroundColors.put(State.CALL, Color.YELLOW); mBackgroundColors.put(State.CONTROL, Color.ORANGE); mForegroundColors.put(State.CONTROL, Color.BLUE); mBackgroundColors.put(State.DATA, Color.GREEN); mForegroundColors.put(State.DATA, Color.BLUE); mBackgroundColors.put(State.ENCRYPTED, Color.MAGENTA); mForegroundColors.put(State.ENCRYPTED, Color.WHITE); mBackgroundColors.put(State.FADE, Color.LIGHT_GRAY); mForegroundColors.put(State.FADE, Color.DARK_GRAY); mBackgroundColors.put(State.IDLE, Color.WHITE); mForegroundColors.put(State.IDLE, Color.DARK_GRAY); mBackgroundColors.put(State.RESET, Color.PINK); mForegroundColors.put(State.RESET, Color.YELLOW); mBackgroundColors.put(State.TEARDOWN, Color.DARK_GRAY); mForegroundColors.put(State.TEARDOWN, Color.WHITE); } @Override public void valueChanged(ListSelectionEvent e) { if(!mTable.getSelectionModel().getValueIsAdjusting()) { ProcessingChain processingChain = null; int selectedViewRow = mTable.getSelectedRow(); if(selectedViewRow >= 0) { int selectedModelRow = mTable.convertRowIndexToModel(selectedViewRow); ChannelMetadata selectedMetadata = mChannelProcessingManager.getChannelMetadataModel() .getChannelMetadata(selectedModelRow); if(selectedMetadata != null) { mUserSelectedChannel = mChannelProcessingManager.getChannelMetadataModel() .getChannelFromMetadata(selectedMetadata); processingChain = mChannelProcessingManager.getProcessingChain(mUserSelectedChannel); } } mSelectedProcessingChainBroadcaster.broadcast(processingChain); } } /** * Adds the listener to receive the processing chain associated with the metadata selected in the * metadata table. */ public void addProcessingChainSelectionListener(Listener listener) { mSelectedProcessingChainBroadcaster.addListener(listener); } /** * Cell renderer for frequency values */ public class FrequencyCellRenderer extends DefaultTableCellRenderer { private final DecimalFormat FREQUENCY_FORMATTER = new DecimalFormat( "#.00000" ); public FrequencyCellRenderer() { setHorizontalAlignment(JLabel.CENTER); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { JLabel label = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); if(value instanceof FrequencyConfigurationIdentifier) { long frequency = ((FrequencyConfigurationIdentifier)value).getValue(); label.setText(FREQUENCY_FORMATTER.format(frequency / 1e6d)); } else { label.setText(null); } return label; } } /** * Alias cell renderer */ public class AliasCellRenderer extends DefaultTableCellRenderer { public AliasCellRenderer() { setHorizontalAlignment(JLabel.LEFT); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { JLabel label = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); if(value instanceof List) { List aliases = (List)value; if(!aliases.isEmpty()) { label.setText(Joiner.on(", ").skipNulls().join(aliases)); label.setIcon(mIconModel.getIcon(aliases.get(0).getIconName(), IconModel.DEFAULT_ICON_SIZE)); label.setForeground(aliases.get(0).getDisplayColor()); } else { label.setText(null); label.setIcon(null); label.setForeground(table.getForeground()); } } else { label.setText(null); label.setIcon(null); label.setForeground(table.getForeground()); } return label; } } /** * Abstract cell renderer for identifiers */ public abstract class IdentifierCellRenderer extends DefaultTableCellRenderer { private final static String EMPTY_VALUE = "-----"; private TalkgroupFormatPreference mTalkgroupFormatPreference; public IdentifierCellRenderer(TalkgroupFormatPreference talkgroupFormatPreference) { mTalkgroupFormatPreference = talkgroupFormatPreference; setHorizontalAlignment(JLabel.CENTER); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { JLabel label = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); if(value instanceof ChannelMetadata) { ChannelMetadata channelMetadata = (ChannelMetadata)value; Identifier identifier = getIdentifier(channelMetadata); String text = mTalkgroupFormatPreference.format(identifier); if(text == null || text.isEmpty()) { text = EMPTY_VALUE; } label.setText(text); } else { label.setText(EMPTY_VALUE); } return label; } public abstract Identifier getIdentifier(ChannelMetadata channelMetadata); } /** * Cell renderer for the FROM identifier */ public class FromCellRenderer extends IdentifierCellRenderer { public FromCellRenderer(TalkgroupFormatPreference talkgroupFormatPreference) { super(talkgroupFormatPreference); } @Override public Identifier getIdentifier(ChannelMetadata channelMetadata) { return channelMetadata.getFromIdentifier(); } } /** * Cell renderer for the TO identifier */ public class ToCellRenderer extends IdentifierCellRenderer { public ToCellRenderer(TalkgroupFormatPreference talkgroupFormatPreference) { super(talkgroupFormatPreference); } @Override public Identifier getIdentifier(ChannelMetadata channelMetadata) { return channelMetadata.getToIdentifier(); } } public class ColoredStateCellRenderer extends DefaultTableCellRenderer { public ColoredStateCellRenderer() { setHorizontalAlignment(SwingConstants.CENTER); } @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { JLabel label = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); Color background = table.getBackground(); Color foreground = table.getForeground(); if(value instanceof ChannelStateIdentifier) { State state = ((ChannelStateIdentifier)value).getValue(); label.setText(state.getDisplayValue()); if(mBackgroundColors.containsKey(state)) { background = mBackgroundColors.get(state); } if(mForegroundColors.containsKey(state)) { foreground = mForegroundColors.get(state); } } else { setText("----"); } setBackground(background); setForeground(foreground); return label; } } public class MouseSupport extends MouseAdapter { @Override public void mouseClicked(MouseEvent e) { if(e.getButton() == MouseEvent.BUTTON3) //Right click for context { JPopupMenu popupMenu = new JPopupMenu(); boolean populated = false; int viewRowIndex = mTable.rowAtPoint(e.getPoint()); if(viewRowIndex >= 0) { int modelRowIndex = mTable.convertRowIndexToModel(viewRowIndex); if(modelRowIndex >= 0) { ChannelMetadata metadata = mChannelProcessingManager.getChannelMetadataModel().getChannelMetadata(modelRowIndex); if(metadata != null) { Channel channel = mChannelProcessingManager.getChannelMetadataModel() .getChannelFromMetadata(metadata); if(channel != null) { JMenuItem viewChannel = new JMenuItem("View/Edit: " + channel.getShortTitle()); viewChannel.addActionListener(e2 -> MyEventBus.getGlobalEventBus().post(new ViewChannelRequest(channel))); popupMenu.add(viewChannel); populated = true; } } } } if(!populated) { popupMenu.add(new JMenuItem("No Actions Available")); } popupMenu.show(mTable, e.getX(), e.getY()); } } } /** * Listener to be notified when a channel and associated channel metadata(s) are added to the underlying * channel metadata model. * * When a channel is added, it is compared the the last user selected channel and if they are the same, it * invokes a selection event on the channel metadata row so that the channel metadata is re-selected. This is * primarily a hack to counter-act the DMR Capacity+ REST channel rotation where a channel is converted to a * traffic channel and the previous channel is restarted. The UI effect is that the user selected channel row * in the Now Playing window continually loses selection over the channel and causes the user to perpetually * chase the channel row. */ public class ChannelAddListener implements Listener { @Override public void receive(ChannelAndMetadata channelAndMetadata) { if(mUserSelectedChannel != null && mUserSelectedChannel.getChannelID() == channelAndMetadata.getChannel().getChannelID()) { List metadata = channelAndMetadata.getChannelMetadata(); if(metadata.size() > 0) { int modelRow = mChannelProcessingManager.getChannelMetadataModel().getRow(metadata.get(0)); if(modelRow >= 0) { int tableRow = mTable.convertRowIndexToView(modelRow); mTable.getSelectionModel().setSelectionInterval(tableRow, tableRow); } } } } } }