/* * ALMA - Atacama Large Millimiter Array (c) European Southern Observatory, 2007 * * This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This library 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 Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; if not, write to the Free Software Foundation, Inc., * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ /** * @author acaproni * @version $Id: AlarmTable.java,v 1.25 2012/10/16 09:14:19 acaproni Exp $ * @since */ package alma.acsplugins.alarmsystem.gui.table; import java.awt.Component; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import javax.swing.DefaultListSelectionModel; import javax.swing.ImageIcon; import javax.swing.JCheckBoxMenuItem; import javax.swing.JComponent; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPopupMenu; import javax.swing.JSeparator; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.RowFilter; import javax.swing.RowSorter; import javax.swing.SortOrder; import javax.swing.ToolTipManager; import javax.swing.event.TableModelEvent; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import javax.swing.table.TableRowSorter; import alma.acs.gui.util.threadsupport.EDTExecutor; import alma.acs.util.IsoDateFormat; import alma.acsplugins.alarmsystem.gui.CernSysPanel; import alma.acsplugins.alarmsystem.gui.reduced.ReducedChainDlg; import alma.acsplugins.alarmsystem.gui.statusline.StatusLine; import alma.acsplugins.alarmsystem.gui.table.AlarmTableModel.AlarmTableColumn; import alma.acsplugins.alarmsystem.gui.table.AlarmTableModel.PriorityLabel; import alma.acsplugins.alarmsystem.gui.undocumented.table.UndocAlarmTableModel; import alma.acsplugins.alarmsystem.gui.viewcoordination.ViewCoordinator; import alma.acsplugins.alarmsystem.gui.viewcoordination.ViewCoordinator.AlarmSelectionListener; import alma.alarmsystem.clients.AlarmCategoryClient; import cern.laser.client.data.Alarm; /** * * The table of alarms * */ public class AlarmTable extends JTable implements ActionListener { /** * The mouse adapter receiving mouse events generated * over the table of the alarms * * @author acaproni * */ private class AlarmTableMouseAdapter extends MouseAdapter { /** * The last selected alarm * * It is set when the user presses over a row (i,e. selects an alarm) */ public AlarmTableEntry selectedAlarm; /** * @see MouseListener */ public void mouseClicked(MouseEvent e) { alarmSelected(e); showPopup(e); } /** * @see MouseListener */ public void mousePressed(MouseEvent e) { alarmSelected(e); showPopup(e); } /** * @see MouseListener */ public void mouseReleased(MouseEvent e) { alarmSelected(e); showPopup(e); } /** * The user selected a row, i.e. an alarm. * alarmSected notifies the model that the icon must be removed * <p> * TODO: Check if this can be merged with AlarmTable#changeSelection * * @param e The event to get the selected row from */ private void alarmSelected(MouseEvent e) { int row=rowAtPoint(new Point(e.getX(),+e.getY())); AlarmTable.this.model.alarmSelected(getRowSorter().convertRowIndexToModel(row)); } /** * Show the popup menu * * @param e The mouse event that triggered the pop */ private void showPopup(final MouseEvent e) { if (!e.isPopupTrigger()) { return; } int row=rowAtPoint(new Point(e.getX(),+e.getY())); selectedAlarm = AlarmTable.this.model.getRowAlarm(getRowSorter().convertRowIndexToModel(row)); EDTExecutor.instance().execute(new Runnable() { @Override public void run() { ackMI.setEnabled(!selectedAlarm.getStatus().isActive()); showReducedMI.setEnabled(selectedAlarm!=null && (selectedAlarm.isParent())); popupM.show(e.getComponent(),e.getX(),e.getY()); popupM.setVisible(true); } }); } } /** * The mouse adapter receiving mouse events generated * over the header of the table of the alarms * * @author acaproni * */ private class AlarmHeaderMouseAdapter extends MouseAdapter implements ActionListener { // The popup private JPopupMenu headerPopup = new JPopupMenu("Header"); private JCheckBoxMenuItem[] menuItems; /** * Constructor */ public AlarmHeaderMouseAdapter() { // Build the popup menu // // Each item has the same order of the AlarmTableColumn menuItems = new JCheckBoxMenuItem[AlarmTableColumn.values().length]; int t=0; for (AlarmTableColumn col: AlarmTableColumn.values()) { menuItems[t]= new JCheckBoxMenuItem(col.popupTitle); menuItems[t].addActionListener(this); headerPopup.add(menuItems[t++]); } } /** * @see MouseListener */ public void mouseClicked(MouseEvent e) { showPopup(e); } /** * @see MouseListener */ public void mousePressed(MouseEvent e) { showPopup(e); } /** * @see MouseListener */ public void mouseReleased(MouseEvent e) { showPopup(e); } /** * Show the popup menu * * @param e The mouse event that triggered the pop */ private void showPopup(final MouseEvent e) { if (!e.isPopupTrigger()) { return; } EDTExecutor.instance().execute(new Runnable() { @Override public void run() { ratioMenu(); headerPopup.show(e.getComponent(),e.getX(),e.getY()); } }); } /** * Set/unset all the checkbox depending on the * visible/hidden columns */ private void ratioMenu() { TableColumnModel colModel = getColumnModel(); int pos=0; for (AlarmTableColumn col: AlarmTableColumn.values()) { try { colModel.getColumnIndex(col); menuItems[pos].setSelected(true); } catch (IllegalArgumentException iae) { menuItems[pos].setSelected(false); } pos++; } } /** * @see ActionListener */ public void actionPerformed(ActionEvent e) { // Look for the source of this event JCheckBoxMenuItem source=null; int column=-1; for (int t=0; t<menuItems.length; t++) { if (e.getSource()==menuItems[t]) { source=menuItems[t]; column=t; break; } } if (source==null) { System.out.println("Unknown source of event: "+e.getSource()); return; } addRemoveColumn(AlarmTableColumn.values()[column], source.isSelected()); } } /** * The filter of table entries. * <P> * Accepts all the entries containing the passed string in one of the * visible columns. * <P> * The filter can be applied to hide entries instead of select (i.e. NOT logic) * * @author acaproni * */ private class AlarmTableFilter extends RowFilter<AlarmTableModel,Integer> { /** * If <code>true</code> the selected entries are those that do not * contain the filter string */ private boolean applyAsNot=false; /** * The string to compare to the visible columns to accept the antries */ private String filterString; /** * Set the filter * * @param flt The string used to filter * @param not If <code>true</code> the filter is applied with a NOT policy */ public void setFilter(String flt, boolean not) { if (flt==null || flt.isEmpty()) { throw new IllegalArgumentException("The string for filtering can't be null nor empty"); } applyAsNot=not; filterString=flt; } @Override public boolean include( Entry<? extends AlarmTableModel, ? extends Integer> entry) { boolean ret=false; // TODO Auto-generated method stub TableColumnModel colModel = getColumnModel(); int modelRow=entry.getIdentifier(); for (int t=0; t< colModel.getColumnCount(); t++) { TableColumn tc=colModel.getColumn(t); int idx=tc.getModelIndex(); Object obj = model.getValueAt(modelRow,idx); if (!(obj instanceof String)) { continue; } if (obj.toString().contains(filterString)) { ret= true; } } if (applyAsNot) { return !ret; } return ret; } } /** * The model of the table */ private final AlarmTableModel model; /** * The panel showing this table */ private final CernSysPanel panel; /** * The sorter for sorting the rows of the table */ private TableRowSorter<AlarmTableModel> sorter; /** * The table selection model */ private DefaultListSelectionModel selectionModel; /** * The cols of the table */ private TableColumn[] columns; /** * The alarm adapter that recives events from the mouse */ private AlarmTableMouseAdapter mouseAdapter = new AlarmTableMouseAdapter(); /** * The filter of the table activate from the toolbar * <P> * This filter is added or removed from the tale filters depending if the * user select or unselect the toolbar button */ private final AlarmTableFilter filter = new AlarmTableFilter(); /** * The clipboard */ private ClipboardHelper clipboard = new ClipboardHelper(); /** * The popup menu shown when the user presses the right mouse button over a row */ private JPopupMenu popupM = new JPopupMenu("Alarm"); /** * The menu item to acknowledge an alarm */ private JMenuItem ackMI = new JMenuItem("Acknowledge"); /** * The menu item to save */ private JMenuItem saveMI = new JMenuItem("Save..."); /** * The menu item to svae the selected alarm into the clipboard */ private JMenuItem clipMI = new JMenuItem("To clipboard"); /** * The menu to show the reduction chain of an alarm */ private JMenuItem showReducedMI = new JMenuItem("Show reduction chain"); /** * The label returned as renderer when no flag is shown in the * first column of the table */ private JLabel emptyLbl = new JLabel(); /** * The dialog showing the table with the alarms involved in a * reduction chain */ private ReducedChainDlg reducedDlg=null; /** * The engine to search alarm entries in the table */ private final SearchEngine searchEngine; /** * The ID of the last selected alarm for painting in bold */ private String selectedAlarmId=null; /** * The renderer for the reduced alarm entries i.e. * the entries normally hidden */ public static final JLabel reductionRenderer = new JLabel( new ImageIcon(AlarmGUIType.class.getResource(AlarmGUIType.iconFolder+"arrow_in.png")), JLabel.CENTER); /** * The renderer for a node that hides children because of a * reduction rule is in place */ public static final JLabel hasReducedNodesRenderer = new JLabel( new ImageIcon(AlarmGUIType.class.getResource(AlarmGUIType.iconFolder+"add.png")), JLabel.CENTER); /** * The undocumented table model */ private final UndocAlarmTableModel undocModel; private volatile AlarmSelectionListener listener; private final StatusLine statusLine; /** * Constructor * @param model The model for this table * @param panel The panel showing this table * @param statusLine Status line ref, needed to update filter status. * If this direct ref seems too dirty, we could perhaps let CernSysPanel * register for filter changes and propagate that info to the status line. */ public AlarmTable(AlarmTableModel model, CernSysPanel panel, UndocAlarmTableModel undocModel, StatusLine statusLine) { super(model); if (model==null) { throw new IllegalArgumentException("Invalid null model in constructor"); } this.model=model; if (undocModel==null) { throw new IllegalArgumentException("Invalid null undocumented model in constructor"); } this.undocModel=undocModel; if (panel==null) { throw new IllegalArgumentException("Invalid null panel in constructor"); } this.panel=panel; EDTExecutor.instance().execute(new Runnable() { public void run() { initGUI(); } }); this.statusLine = statusLine; searchEngine=new SearchEngine(this,model); } /** * Init the GUI */ private void initGUI() { setShowHorizontalLines(true); // Build and set the selection model selectionModel = new DefaultListSelectionModel(); selectionModel.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); setSelectionModel(selectionModel); this.setOpaque(false); sorter = new TableRowSorter<AlarmTableModel>(model); this.setRowSorter(sorter); sorter.setMaxSortKeys(2); sorter.setSortsOnUpdates(true); // Initially sort by timestamp List<RowSorter.SortKey> sortKeys = new ArrayList<RowSorter.SortKey>(); sortKeys.add(new RowSorter.SortKey(AlarmTableColumn.PRIORITY.ordinal(), SortOrder.ASCENDING)); sortKeys.add(new RowSorter.SortKey(AlarmTableColumn.TIME.ordinal(), SortOrder.DESCENDING)); sorter.setSortKeys(sortKeys); // Remove all the columns not visible at startup TableColumnModel colModel = getColumnModel(); columns = new TableColumn[colModel.getColumnCount()]; for (int t=0; t<columns.length; t++) { columns[t]=colModel.getColumn(t); columns[t].setIdentifier(AlarmTableColumn.values()[t]); if (columns[t].getIdentifier()==AlarmTableColumn.ICON || columns[t].getIdentifier()==AlarmTableColumn.IS_CHILD || columns[t].getIdentifier()==AlarmTableColumn.IS_PARENT) { columns[t].setWidth(20); columns[t].setResizable(false); columns[t].setPreferredWidth(20); columns[t].setMaxWidth(20); columns[t].setMinWidth(20); } else if (columns[t].getIdentifier()==AlarmTableColumn.PRIORITY) { BufferedImage bImg = new BufferedImage(100,100,BufferedImage.TYPE_INT_RGB); Graphics2D g2D= bImg.createGraphics(); FontMetrics fm=g2D.getFontMetrics(); int sz=fm.stringWidth(PriorityLabel.VERY_HIGH.description); columns[t].setPreferredWidth(sz+6); columns[t].setMaxWidth(sz+8); } } for (AlarmTableColumn col: AlarmTableColumn.values()) { if (!col.visibleAtStartup) { colModel.removeColumn(columns[col.ordinal()]); } } buildPopupMenu(); addMouseListener(mouseAdapter); getTableHeader().addMouseListener(new AlarmHeaderMouseAdapter()); // Set the tooltip ToolTipManager ttm = ToolTipManager.sharedInstance(); ttm.setDismissDelay(Integer.MAX_VALUE); ttm.setLightWeightPopupEnabled(true); } /** * Build the popup menu */ private void buildPopupMenu() { popupM.add(ackMI); popupM.add(showReducedMI); popupM.add(new JSeparator()); popupM.add(saveMI); popupM.add(clipMI); popupM.pack(); ackMI.addActionListener(this); saveMI.addActionListener(this); clipMI.addActionListener(this); showReducedMI.addActionListener(this); } /** * @see JTable */ @Override public Component prepareRenderer(TableCellRenderer renderer, int rowIndex, int vColIndex) { TableColumn col = getColumnModel().getColumn(vColIndex); AlarmTableEntry entry=null; try { entry = model.getRowEntry(sorter.convertRowIndexToModel(rowIndex)); } catch (Throwable t) { // This can happen if the entry has been removed by the thread while // this method runs. entry=null; } if (entry==null) { return emptyLbl; } if (col.getIdentifier().equals(AlarmTableColumn.ICON)) { if (model.isRowAlarmNew(sorter.convertRowIndexToModel(rowIndex))) { return AlarmGUIType.fromAlarm(entry).flagRenderer; } else { return emptyLbl; } } else if (col.getIdentifier().equals(AlarmTableColumn.IS_CHILD)) { if (entry.isChild()) { return AlarmTable.reductionRenderer; } else { return emptyLbl; } } else if (col.getIdentifier().equals(AlarmTableColumn.IS_PARENT)) { if (entry.isParent()) { return AlarmTable.hasReducedNodesRenderer; } else { return emptyLbl; } } Component c = super.prepareRenderer(renderer, rowIndex, vColIndex); if (entry.getAlarmId().equals(selectedAlarmId)) { Font f = c.getFont(); Font bold=f.deriveFont(Font.BOLD); c.setFont(bold); } colorizeCell(c, entry); if (c instanceof JComponent) { JComponent jc = (JComponent) c; if (((AlarmTableModel)model).getCellContent(sorter.convertRowIndexToModel(rowIndex), convertColumnIndexToModel(vColIndex))==null) { jc.setToolTipText(null); } else { jc.setToolTipText("<HTML>"+((AlarmTableModel)model).getCellContent(sorter.convertRowIndexToModel(rowIndex), convertColumnIndexToModel(vColIndex))); } } return c; } /** * Set the background and the foreground of the component depending * on the priority and the state of the passed alarm * * @param c The component to color * @param priority The alarm to set the color */ private void colorizeCell(final Component c, final AlarmTableEntry alarm) { EDTExecutor.instance().execute(new Runnable() { public void run() { AlarmGUIType alarmType = AlarmGUIType.fromAlarm(alarm); c.setForeground(alarmType.foreg); c.setBackground(alarmType.backg); } }); } /** * @see ActionListener */ public void actionPerformed(ActionEvent e) { if (e.getSource()==saveMI) { saveAlarm(mouseAdapter.selectedAlarm); } else if (e.getSource()==clipMI) { clipboard.setClipboardContents(mouseAdapter.selectedAlarm.toString()); } else if (e.getSource()==ackMI) { model.acknowledge(mouseAdapter.selectedAlarm); } else if (e.getSource()==showReducedMI) { showReductionChain(mouseAdapter.selectedAlarm); } } /** * Save the alarm in a file * * @param The alarm to save in a plain text file */ public void saveAlarm(final Alarm alarm) { // Get the user dir property final JFileChooser fileChooser = new JFileChooser(); if (fileChooser.showSaveDialog(this)!=JFileChooser.APPROVE_OPTION) { return; } // Save the file new Thread(new Runnable(){ public void run() { // Build the text to write SimpleDateFormat dateFormat = new IsoDateFormat(); StringBuilder str = new StringBuilder(alarm.toString()); str.append("\n\n"); str.append("Saved at "); str.append(dateFormat.format(new Date(System.currentTimeMillis()))); str.append("\n\n"); // Save the file File outF = fileChooser.getSelectedFile(); FileOutputStream fOutS; try { fOutS = new FileOutputStream(outF,false); fOutS.write(str.toString().getBytes()); fOutS.flush(); fOutS.close(); } catch (Exception e) { JOptionPane.showInternalMessageDialog(AlarmTable.this, e.getMessage(), "Error saving", JOptionPane.ERROR_MESSAGE); } } },"SaveThread").start(); } /** * Free all the resource */ public void close() { if (reducedDlg!=null) { reducedDlg.close(); } } /** * Show the dialog with all the nodes reduced by the passed alarm * * @param alarm The alarm whose children must be shown in a dialog */ private void showReductionChain(AlarmTableEntry alarm) { if (alarm==null) { throw new IllegalArgumentException("The alarm can't be null"); } AlarmCategoryClient client = model.getCategoryClient(); if (reducedDlg==null) { reducedDlg = new ReducedChainDlg(client,alarm,panel,undocModel); } else { reducedDlg.setRootAlarm(alarm); EDTExecutor.instance().execute(new Runnable() { public void run() { reducedDlg.setVisible(true); } }); } } /** * Set the visible columns in the table. * The columns are displayed following their order in the array. * * @param cols The visible columns in the table; * it can't be <code>null</code> and at least one column must be in the array. */ public void showColumns(final AlarmTableColumn[] cols) { if (cols==null || cols.length==0) { throw new IllegalArgumentException("Invalid columns array"); } EDTExecutor.instance().execute(new Runnable() { public void run() { TableColumnModel colModel = getColumnModel(); // Remove all the columns for (TableColumn column: columns) { colModel.removeColumn(column); } for (AlarmTableColumn aTC: cols) { for (int t=0; t<columns.length; t++) { if (columns[t].getIdentifier()==aTC) { colModel.addColumn(columns[t]); break; } } } } }); } /** * Add/remove one column from the table * * @param col The column to add or remove * @param add If <code>true</code> add the column, otherwise remove the column */ public void addRemoveColumn(final AlarmTableColumn col, final boolean add) { if (col==null) { throw new IllegalArgumentException("The column to add/remove can't be null"); } EDTExecutor.instance().execute(new Runnable() { public void run() { TableColumnModel colModel = getColumnModel(); if (add) { colModel.addColumn(columns[col.ordinal()]); } else { colModel.removeColumn(columns[col.ordinal()]); } } }); } /** * Search for a string in the table * * @param string The string to search in the table * @param next If <code>true</code> search for the next entry * @return <code>true</code> if an entry has been found * * @see SearchEngine */ public boolean search(String string, boolean next) { int ret= searchEngine.search(string, next); if (ret!=-1) { changeSelection(ret, 1, false, false); panel.showMessage("Entry found at "+ret, false); } else { panel.showMessage("No alarm found",true); } return ret!=-1; } /** * Override {@link JTable#changeSelection(int, int, boolean, boolean)} to show * the selected alarm in the detail panel. */ @Override public void changeSelection(int rowIndex, int columnIndex, boolean toggle, boolean extend) { super.changeSelection(rowIndex, columnIndex, toggle, extend); int idx = sorter.convertRowIndexToModel(rowIndex); AlarmTableEntry alarmEntry = model.getAlarmAt(idx); panel.showAlarmDetails(alarmEntry); selectedAlarmId = alarmEntry.getAlarmId(); AlarmSelectionListener listenerCopy = listener; // to avoid concurrency issues if (listenerCopy != null) { Alarm alarm = alarmEntry.getEncapsulatedAlarm(); if (alarm != null) { listenerCopy.notifyAlarmSelected(alarm); } } } /** * This is used only by {@link ViewCoordinator}. */ public void setAlarmSelectionListener(AlarmSelectionListener listener) { this.listener = listener; } /** * Filter the table by the passed string * <P> * Filtering select all the rows containing the passed string * in at least on one (visible) column. * * @param filterString The string used to filter; * if <code>null</code> or empty, the table is unfiltered * @param not If the filter must be applied to discard entries instead of to select */ public void filter(String filterString, boolean not) { if (filterString==null || filterString.isEmpty()) { // remove the filter sorter.setRowFilter(null); } else { // add the filter filter.setFilter(filterString, not); sorter.setRowFilter(filter); } if (statusLine != null) { statusLine.setTableFilterLbl(filterString); } } /** * Return the id of the selected alarm; * the id is <code>null</code> if no alarm is selected; * <P> * This method does not ensure that the alarm with the ID * returned by this method is still in the table: it might have been * removed when the panel discards alarms. * * @return The id of the selected alarm or <code>null</code> * if no alarm is selected */ private String getSelectedAlarmId() { return selectedAlarmId; } }