org.yamcs.ui.packetviewer.PacketsTable Maven / Gradle / Ivy
package org.yamcs.ui.packetviewer;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JTable;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.RowFilter;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableRowSorter;
import org.yamcs.ContainerExtractionResult;
import org.yamcs.TmPacket;
import org.yamcs.mdb.ContainerProcessingResult;
import org.yamcs.mdb.Mdb;
import org.yamcs.mdb.XtceTmExtractor;
import org.yamcs.parameter.ParameterValueList;
import org.yamcs.ui.packetviewer.filter.PacketFilter;
import org.yamcs.utils.TimeEncoding;
import org.yamcs.utils.ValueComparator;
import org.yamcs.xtce.Parameter;
import org.yamcs.xtce.SequenceContainer;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.stream.JsonWriter;
public class PacketsTable extends JTable implements ListSelectionListener {
private static final long serialVersionUID = 1L;
private static final String MARK_PACKET = "Mark Packet";
private static final String UNMARK_PACKET = "Unmark Packet";
private static final Color LIGHT_GRAY = new Color(216, 216, 216);
// Expose action keys (for easier installing in JMenuBar)
public static final String TOGGLE_MARK_ACTION_KEY = "toggle-mark";
public static final String GO_TO_PACKET_ACTION_KEY = "go-to-packet";
public static final String BACK_ACTION_KEY = "back";
public static final String FORWARD_ACTION_KEY = "forward";
public static final String UP_ACTION_KEY = "up";
public static final String DOWN_ACTION_KEY = "down";
public static final String PREF_COLNAMES = "columns";
private PacketsTableModel tableModel;
private TableRowSorter rowSorter;
private PacketFilter packetFilter;
private PacketViewer packetViewer;
private JPopupMenu popup;
private JMenuItem markPacketMenuItem;
private int maxLines = 1000;
private Set markedPacketNrs = new HashSet<>(2);
// Store history of previously visited packet numbers
private List history = new ArrayList<>();
private int historyPosition = -1;
// used for extracting parameters shown on the left overview table
XtceTmExtractor tmExtractor;
LinkedHashSet columnParaNames;
public PacketsTable(PacketViewer packetViewer) {
super();
this.packetViewer = packetViewer;
readColumnsFromPreference();
tableModel = new PacketsTableModel();
setModel(tableModel);
setPreferredScrollableViewportSize(new Dimension(400, 400));
setFillsViewportHeight(true);
getColumnModel().getColumn(0).setPreferredWidth(50);
getColumnModel().getColumn(1).setPreferredWidth(160);
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
setShowHorizontalLines(false);
setGridColor(new Color(216, 216, 216));
setIntercellSpacing(new Dimension(0, 0));
setRowHeight(getRowHeight() + 2);
rowSorter = new TableRowSorter<>(tableModel);
setRowSorter(rowSorter);
configureRowSorting();
// Swing highlights the selected cell with an annoying blue border.
// Disable this behaviour by using a custom cell renderer
setDefaultRenderer(Object.class, new DefaultTableCellRenderer() {
private static final long serialVersionUID = 1L;
@Override
public Component getTableCellRendererComponent(JTable table,
Object value, boolean isSelected, boolean hasFocus,
int row, int column) {
return super.getTableCellRendererComponent(table, value,
isSelected, false /* disable focus ! */, row, column);
}
});
DefaultTableCellRenderer numberRenderer = new DefaultTableCellRenderer() {
private static final long serialVersionUID = 1L;
@Override
public Component getTableCellRendererComponent(JTable table,
Object value, boolean isSelected, boolean hasFocus,
int row, int column) {
return super.getTableCellRendererComponent(table, value,
isSelected, false /* disable focus ! */, row, column);
}
};
numberRenderer.setHorizontalAlignment(SwingConstants.RIGHT);
setDefaultRenderer(Number.class, numberRenderer);
createActions();
installPopupMenus();
}
// It seems like this needs to be re-done after every model restructuring
public void configureRowSorting() {
rowSorter.setComparator(2, (ListPacket o1, ListPacket o2) -> {
return o1.getName().compareTo(o2.getName());
});
for (int i = 3; i < getColumnCount(); i++) {
rowSorter.setComparator(i, new ValueComparator());
}
}
public void configureRowFilter(PacketFilter packetFilter) {
this.packetFilter = packetFilter;
RowFilter rf = null;
if (packetFilter != null) {
rf = new RowFilter<>() {
@Override
public boolean include(Entry extends PacketsTableModel, ? extends Object> entry) {
ListPacket packet = (ListPacket) entry.getValue(2);
return packetFilter.matches(packet);
}
};
for (Parameter parameter : packetFilter.getParameters()) {
tmExtractor.startProviding(parameter);
}
}
rowSorter.setRowFilter(rf);
}
@Override
public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
Component component = super.prepareRenderer(renderer, row, column);
if (popup.isShowing() && isCellSelected(row, column)) {
component.setBackground(LIGHT_GRAY);
} else if (!isCellSelected(row, column)) {
row = convertRowIndexToModel(row);
int packetNr = (Integer) tableModel.getValueAt(row, 0);
if (markedPacketNrs.contains(packetNr)) {
component.setBackground(Color.YELLOW);
} else {
component.setBackground(Color.WHITE);
}
}
return component;
}
public void clear() {
clearSelection();
tableModel.clear();
markedPacketNrs.clear();
history.clear();
historyPosition = -1;
updateActionStates();
}
public void setMaxLines(int maxLines) {
this.maxLines = maxLines;
}
/**
* Goes back to the previously selected packet
*/
public void goBack() {
if (historyPosition > 0) {
historyPosition--;
goToPacket(history.get(historyPosition));
}
}
/**
* Goes forward to the packet that was selected before the {@code goBack()} was used.
*/
public void goForward() {
if (historyPosition < history.size() - 1) {
historyPosition++;
goToPacket(history.get(historyPosition));
}
}
/**
* Goes to the packet that visually succeeds the currently selected packet
*/
public void goUp() {
int rowIndex = getSelectedRow();
if (rowIndex != -1) {
if (rowIndex > 0) {
rowIndex = rowIndex - 1;
setRowSelectionInterval(rowIndex, rowIndex);
scrollRectToVisible(getCellRect(rowIndex, 0, true));
}
} else if (getRowCount() > 0) {
setRowSelectionInterval(0, 0);
scrollRectToVisible(getCellRect(0, 0, true));
}
}
/**
* Goes to the packet that visually succeeds the currently selected packet
*/
public void goDown() {
int rowIndex = getSelectedRow();
if (rowIndex != -1) {
if (rowIndex < getRowCount() - 1) {
rowIndex = rowIndex + 1;
setRowSelectionInterval(rowIndex, rowIndex);
scrollRectToVisible(getCellRect(rowIndex, 0, true));
}
} else if (getRowCount() > 0) {
setRowSelectionInterval(0, 0);
scrollRectToVisible(getCellRect(0, 0, true));
}
}
/**
* Jumps to the specified packet number. Note that packet numbers do not necessarily start at 1. When connecting to
* a Yamcs instance, only the latest 1000 packets are displayed.
*/
public void goToPacket(int packetNumber) {
int firstPacketNumber = getPacketNumberRange()[0];
packetNumber = packetNumber - firstPacketNumber;
int rowIndex = convertRowIndexToView(packetNumber);
setRowSelectionInterval(rowIndex, rowIndex);
scrollRectToVisible(getCellRect(rowIndex, 0, true));
}
public int[] getPacketNumberRange() {
int lo = (Integer) getModel().getValueAt(0, 0);
int hi = lo + getRowCount() - 1;
return new int[] { lo, hi };
}
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
private void createActions() {
// Ctrl on win/linux, Command on mac
int menuKey = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
//
// GO TO PACKET
Action goToPacketAction = new AbstractAction("Go to Packet...") {
private static final long serialVersionUID = 1L;
private GoToPacketDialog goToPacketDialog;
@Override
public void actionPerformed(ActionEvent e) {
if (goToPacketDialog == null) {
goToPacketDialog = new GoToPacketDialog(PacketsTable.this);
}
int ret = goToPacketDialog.showDialog();
if (ret == GoToPacketDialog.APPROVE_OPTION) {
goToPacket(goToPacketDialog.getLineNumber());
}
}
};
goToPacketAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_P);
goToPacketAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_G, menuKey));
getActionMap().put(GO_TO_PACKET_ACTION_KEY, goToPacketAction);
//
// BACK
Action backAction = new AbstractAction("Back") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
goBack();
}
};
backAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_B);
backAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, ActionEvent.ALT_MASK));
getActionMap().put(BACK_ACTION_KEY, backAction);
//
// FORWARD
Action forwardAction = new AbstractAction("Forward") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
goForward();
}
};
forwardAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_F);
forwardAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, ActionEvent.ALT_MASK));
getActionMap().put(FORWARD_ACTION_KEY, forwardAction);
//
// UP
Action upAction = new AbstractAction("Previous Packet") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
goUp();
}
};
upAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_P);
upAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_UP, ActionEvent.ALT_MASK));
getActionMap().put(UP_ACTION_KEY, upAction);
//
// DOWN
Action downAction = new AbstractAction("Next Packet") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
goDown();
}
};
downAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_N);
downAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, ActionEvent.ALT_MASK));
getActionMap().put(DOWN_ACTION_KEY, downAction);
//
// TOGGLE MARK
Action toggleMarkAction = new AbstractAction(MARK_PACKET) {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
int rowIndex = getSelectedRow();
if (rowIndex != -1) {
rowIndex = convertRowIndexToModel(rowIndex);
int packetNr = (Integer) getModel().getValueAt(rowIndex, 0);
if (markedPacketNrs.contains(packetNr)) {
markedPacketNrs.remove(packetNr);
} else {
markedPacketNrs.add(packetNr);
}
}
}
};
toggleMarkAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_M);
toggleMarkAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_M, menuKey));
getActionMap().put(TOGGLE_MARK_ACTION_KEY, toggleMarkAction);
updateActionStates();
}
private void installPopupMenus() {
// Header
getTableHeader().addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
maybeShowPopup(e);
}
@Override
public void mouseReleased(MouseEvent e) {
maybeShowPopup(e);
}
private void maybeShowPopup(MouseEvent e) {
if (e.isPopupTrigger()) {
int columnIndex = convertColumnIndexToModel(columnAtPoint(e.getPoint()));
if (columnIndex >= 0) {
ColumnHeaderPopUp menu = new ColumnHeaderPopUp(columnIndex);
menu.show(e.getComponent(), e.getX(), e.getY());
}
}
}
});
// Content
popup = new JPopupMenu();
markPacketMenuItem = new JMenuItem(getActionMap().get(TOGGLE_MARK_ACTION_KEY));
popup.add(markPacketMenuItem);
popup.addPopupMenuListener(new PopupMenuListener() {
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
// Make sure selection background of entire row is updated
// Not just the parts that were covered by the popup.
repaint();
}
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
}
@Override
public void popupMenuCanceled(PopupMenuEvent e) {
}
});
addMouseListener(new PopupListener());
}
@Override
public void valueChanged(ListSelectionEvent e) {
super.valueChanged(e);
if (!e.getValueIsAdjusting()) {
int rowIndex = getSelectedRow();
if (rowIndex != -1) {
rowIndex = convertRowIndexToModel(rowIndex);
packetViewer.setSelectedPacket((ListPacket) getModel().getValueAt(rowIndex, 2));
int packetNumber = (Integer) getModel().getValueAt(rowIndex, 0);
if (history.isEmpty() || history.get(historyPosition) != packetNumber) {
historyPosition++;
history.add(historyPosition, packetNumber);
// Clear Forward history (if any)
for (int i = history.size() - 1; i > historyPosition; i--) {
history.remove(i);
}
// Limit total history size to 10
if (history.size() > 10) {
history.remove(0);
historyPosition--;
}
}
}
updateActionStates();
}
}
private void updateActionStates() {
// Reflect selection to mark/unmark actions
int rowIndex = getSelectedRow();
Action toggleMark = getActionMap().get(TOGGLE_MARK_ACTION_KEY);
if (rowIndex == -1) {
toggleMark.putValue(Action.NAME, MARK_PACKET);
toggleMark.setEnabled(false);
} else {
toggleMark.setEnabled(true);
rowIndex = convertRowIndexToModel(rowIndex);
int packetNr = (Integer) getModel().getValueAt(rowIndex, 0);
if (markedPacketNrs.contains(packetNr)) {
toggleMark.putValue(Action.NAME, UNMARK_PACKET);
} else {
toggleMark.putValue(Action.NAME, MARK_PACKET);
}
}
// Activate "Go to Packet" only for non-empty packet table
Action goToPacket = getActionMap().get(GO_TO_PACKET_ACTION_KEY);
goToPacket.setEnabled(getRowCount() > 0);
// Update enabled-state of Back-action
Action back = getActionMap().get(BACK_ACTION_KEY);
back.setEnabled(historyPosition > 0);
// Update enabled-state of Forward-action
Action forward = getActionMap().get(FORWARD_ACTION_KEY);
forward.setEnabled(historyPosition < history.size() - 1);
// Update enabled-state of Up-action
Action up = getActionMap().get(UP_ACTION_KEY);
up.setEnabled(getRowCount() > 0);
// Update enabled-state of Down-action
Action down = getActionMap().get(DOWN_ACTION_KEY);
down.setEnabled(getRowCount() > 0);
}
/**
* @param rowIndex
* row index in model, not in view
*/
private void removeRow(int rowIndex) {
int packetNr = (Integer) tableModel.getValueAt(rowIndex, 0);
markedPacketNrs.remove(packetNr);
int historyIndex = history.indexOf(packetNr);
if (historyIndex != -1) {
history.remove(historyIndex);
if (historyIndex <= historyPosition) {
historyPosition--;
}
}
tableModel.removeRow(0);
}
public void packetReceived(TmPacket data) {
byte[] buf = data.getPacket();
int len = buf.length;
final ListPacket packet = new ListPacket(buf, len);
long gentime = data.getGenerationTime();
packet.setGenerationTime(gentime);
SequenceContainer rootContainer = packetViewer.getCurrentRootContainer();
ContainerProcessingResult cpr = tmExtractor.processPacket(buf, gentime, TimeEncoding.getWallclockTime(),
data.getSeqCount(), rootContainer);
ParameterValueList pvlist = cpr.getParameterResult();
packet.setColumnParameters(pvlist);
List containers = cpr.getContainerResult();
SequenceContainer sc = null;
for (ContainerExtractionResult cer : containers) {
if (cer.getOffset() > 0) {
continue;
}
sc = cer.getContainer();
}
String name;
if (sc == null) {
name = "unknown";
} else {
String alias = packetViewer.getDefaultNamespace() == null ? null
: sc.getAlias(packetViewer.getDefaultNamespace());
name = alias == null ? sc.getQualifiedName() : alias;
}
packet.setName(name);
SwingUtilities.invokeLater(() -> {
tableModel.addPacket(packet);
if (getRowCount() == 1) {
updateActionStates();
}
if (maxLines > 0) {
while (tableModel.getRowCount() > maxLines) {
removeRow(0);
}
}
if (packetViewer.miAutoScroll.isSelected() && getRowCount() > 0) {
int rowNum = convertRowIndexToModel(getRowCount() - 1);
Rectangle rect = getCellRect(rowNum, 0, true);
scrollRectToVisible(rect);
}
if (packetViewer.miAutoSelect.isSelected() && getRowCount() > 0) {
int rowNum = getRowCount() - 1;
getSelectionModel().setSelectionInterval(rowNum, rowNum);
}
});
}
// reads from the preferences the columns (parameters) that have to be shown in the left table
private void readColumnsFromPreference() {
columnParaNames = new LinkedHashSet<>();
String json = packetViewer.uiPrefs.get(PREF_COLNAMES, null);
if (json == null) {
log("No columns definition found in ui preferences");
return;
}
try {
JsonArray arr = JsonParser.parseString(json).getAsJsonArray();
for (JsonElement name : arr) {
columnParaNames.add(name.getAsString());
}
} catch (Exception e) {
log("Failed to read parameter columns from preferences");
e.printStackTrace();
}
}
private void saveColumnsToPreference() {
try {
StringWriter sw = new StringWriter();
try (JsonWriter writer = new JsonWriter(sw)) {
writer.beginArray();
for (String s : columnParaNames) {
writer.value(s);
}
writer.endArray();
}
packetViewer.uiPrefs.put(PREF_COLNAMES, sw.toString());
} catch (Exception e) {
log("Failed to write parameter columns to preferences");
e.printStackTrace();
}
}
/**
* adds columns corresponding to existing parameters in XtceDB also creates the tmExtractor and subscribes to those
* parameters
*
*/
void setupParameterColumns() {
Mdb mdb = packetViewer.mdb;
tmExtractor = new XtceTmExtractor(mdb);
resetDynamicColumns();
for (String pn : columnParaNames) {
Parameter p = packetViewer.mdb.getParameter(pn);
if (p == null) {
// log("Cannot find a parameter with name " + pn + " in XtceDB, ignoring");
} else {
tableModel.addParameterColumn(p);
configureRowSorting();
tmExtractor.startProviding(p);
}
}
if (packetFilter != null) {
for (Parameter parameter : packetFilter.getParameters()) {
tmExtractor.startProviding(parameter);
}
}
SequenceContainer rootsc = mdb.getRootSequenceContainer();
if (mdb.getInheritingContainers(rootsc) == null) {
tmExtractor.startProviding(rootsc);
} else {
for (SequenceContainer sc : mdb.getInheritingContainers(rootsc)) {
tmExtractor.startProviding(sc);
}
}
}
void addParameterColumn(Parameter p) {
if (columnParaNames.add(p.getQualifiedName())) {
saveColumnsToPreference();
tableModel.addParameterColumn(p);
configureRowSorting();
tmExtractor.startProviding(p);
}
}
/**
* Doesn't remove columns from preferences, but resets GUI. Used on start-up, and when changing connections.
*/
void resetDynamicColumns() {
List toDelete = new ArrayList<>();
for (int i = tableModel.getFixedColumnsSize(); i < tableModel.getColumnCount(); i++) {
toDelete.add(getColumnModel().getColumn(i));
}
toDelete.forEach(c -> getColumnModel().removeColumn(c));
tableModel.resetParameterColumns();
configureRowSorting();
}
// index in model
void hideParameterColumn(int columnIndex) {
TableColumn tableColumn = getColumnModel().getColumn(columnIndex);
getColumnModel().removeColumn(tableColumn);
Parameter p = tableModel.getParameter(columnIndex);
columnParaNames.remove(p.getQualifiedName());
tableModel.removeColumn(columnIndex);
saveColumnsToPreference();
configureRowSorting();
}
public void exception(final Exception e) {
packetViewer.log(e.toString());
}
private class PopupListener extends MouseAdapter {
@Override
public void mousePressed(MouseEvent e) {
maybeShowPopup(e);
}
@Override
public void mouseReleased(MouseEvent e) {
maybeShowPopup(e);
}
private void maybeShowPopup(MouseEvent e) {
if (e.isPopupTrigger()) {
int row = rowAtPoint(e.getPoint());
if (row >= 0 && row < getRowCount()) {
setRowSelectionInterval(row, row);
} else {
clearSelection();
}
if (getSelectedRow() < 0) {
return;
}
if (e.isPopupTrigger() && e.getComponent() instanceof JTable) {
popup.show(e.getComponent(), e.getX(), e.getY());
repaint(); // !
}
}
}
}
private class ColumnHeaderPopUp extends JPopupMenu {
private static final long serialVersionUID = 1L;
public ColumnHeaderPopUp(int column) {
JMenuItem hideColumnItem = new JMenuItem("Hide Column");
hideColumnItem.addActionListener(e -> {
hideParameterColumn(column);
});
hideColumnItem.setEnabled(column >= tableModel.getFixedColumnsSize());
add(hideColumnItem);
}
}
public void log(final String s) {
packetViewer.log(s);
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy