/* * [VerCheckTableModel.java] * * Summary: VerCheckTableModel : TableModel, JTable ArrayList for App data. * * Copyright: (c) 2007-2017 Roedy Green, Canadian Mind Products, http://mindprod.com * * Licence: This software may be copied and used freely for any purpose but military. * http://mindprod.com/contact/nonmil.html * * Requires: JDK 1.8+ * * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/ * * Version History: * 1.0 2012-07-20 initial release */ package com.mindprod.vercheck; import com.mindprod.common18.BigDate; import com.mindprod.common18.FontFactory; import javax.swing.DefaultCellEditor; import javax.swing.ImageIcon; import javax.swing.JLabel; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.table.AbstractTableModel; import javax.swing.table.TableCellEditor; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import java.awt.Color; import java.awt.Font; import java.awt.Rectangle; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import static java.lang.System.*; /** * VerCheckTableModel : TableModel, JTable ArrayList for App data. * * @author Roedy Green, Canadian Mind Products * @version 1.0 2012-07-20 initial release * @since 2012-07-20 */ class VerCheckTableModel extends AbstractTableModel implements Iterable { /** * column in model for SNI */ private static final int COL_FOR_SNI = 5; /** * column in model for URL */ private static final int COL_FOR_URL = 6; /** * column in model for app name */ private static final int COL_FOR_APP = 1; /** * column in model for the date released string */ private static final int COL_FOR_DATE_RELEASED = 3; /** * column in model for description */ private static final int COL_FOR_DESCRIPTION = 4; /** * column in model for the marker string */ private static final int COL_FOR_MARKER = 7; /** * column in model for state icon */ private static final int COL_FOR_STATE = 0; /** * column in model for version */ private static final int COL_FOR_VERSION = 2; /** * width margin between columns in pixels */ private static final int COL_MARGIN = 5; /** * width of application name column in pixels */ private static final int COL_WIDTH_FOR_APP = 150; /** * width of date released column in pixels */ private static final int COL_WIDTH_FOR_DATE_RELEASED = 92; /** * width of description pixels */ private static final int COL_WIDTH_FOR_DESCRIPTION = 300; /** * width of marker column in pixels */ private static final int COL_WIDTH_FOR_MARKER = 50; /** * width of Application state icon column in pixels */ private static final int COL_WIDTH_FOR_STATE = 36; /** * width of url column in pixels */ private static final int COL_WIDTH_FOR_URL = 150; /** * width of version column in pixels */ private static final int COL_WIDTH_FOR_VERSION = 75; /** * width of SNI column in pixels */ private static final int COL_WIDTH_FOR_SNI = 50; /** * initial capacity in rows of ArrayList backing the TableModel */ private static final int INITIAL_ROW_CAPACITY = 100; /** * number of columns in the model, 0..6 */ private static final int NUMBER_OF_COLS = 8; /** * column headings state, app, version, date released, url, marker */ private static final String[] COL_NAMES = { "-", "App", "Version", "Released", "Description", "SNI", "URL", "Marker" }; /** * classes for each column, state, appName, version, date, description, url, marker, */ private static final Class[] COLUMN_CLASSES = { ImageIcon.class, String.class, String.class, BigDate.class, String.class, Boolean.class, URL.class, String.class, }; /** * when editing */ private static final Color BACKGROUND_FOR_EDITING = new Color( 0xfffff0 ); /** * colour to temporarily set background to when selected. Leave foreground colour alone */ private static final Color BACKGROUND_FOR_SELECTION = new Color( 0xb8cfe5 ); /** * APP colour */ private static final Color FOREGROUND_FOR_APP = new Color( 0x000089 ); /** * Date Released String colour */ private static final Color FOREGROUND_FOR_DATE_RELEASED = new Color( 0x000089 ); /** * Description colour */ private static final Color FOREGROUND_FOR_DESCRIPTION = new Color( 0x000089 ); /** * when editing */ private static final Color FOREGROUND_FOR_EDITING = new Color( 0x000044 ); /** * Marker String colour */ private static final Color FOREGROUND_FOR_MARKER = new Color( 0x000089 ); /** * SNI colour */ private static final Color FOREGROUND_FOR_SNI = new Color( 0x442222 ); /** * URL colour */ private static final Color FOREGROUND_FOR_URL = new Color( 0x442222 ); /** * Version colour */ private static final Color FOREGROUND_FOR_VERSION = new Color( 0xe00000 ); /** * App font */ private static final Font FONT_FOR_APP_BODY = FontFactory.build( "Dialog", Font.PLAIN, 15 ); /** * Date released font */ private static final Font FONT_FOR_DATE_RELEASED = FontFactory.build( "Dialog", Font.PLAIN, 14 ); /** * Description font */ private static final Font FONT_FOR_DESCRIPTION = FontFactory.build( "Dialog", Font.PLAIN, 14 ); /** * editing font, monospaced to make mousing easier. */ private static final Font FONT_FOR_EDITING = FontFactory.build( "Monospaced", Font.PLAIN, 15 ); /** * marker String font, monospaced to make noticing spaces easier. */ private static final Font FONT_FOR_MARKER = FontFactory.build( "Monospaced", Font.PLAIN, 14 ); /* * SNI font */ private static final Font FONT_FOR_SNI = FontFactory.build( "Dialog", Font.PLAIN, 14 ); /** * URL font */ private static final Font FONT_FOR_URL = FontFactory.build( "Dialog", Font.PLAIN, 14 ); /** * Version font */ private static final Font FONT_FOR_VERSION = FontFactory.build( "Dialog", Font.PLAIN, 14 ); /** * where JTable holds its internal data. */ private ArrayList all_Rows; /** * table of 5 columns icon,appName,url,marker,date released */ private JTable jTable; /** * SelectionModel for the jTable, controls which cell/row selected. */ private ListSelectionModel selectionModel; /** * erase components */ void destroy() { all_Rows = null; jTable = null; selectionModel = null; } /** * get the JTable corresponding GUI */ JTable getJTable() { return jTable; } /** * get the model for selecting rows */ ListSelectionModel getSelectionModel() { return selectionModel; } /** * initialise all components */ void init() { all_Rows = new ArrayList<>( INITIAL_ROW_CAPACITY ); jTable = new JTable( this ); selectionModel = jTable.getSelectionModel(); jTable.setRowHeight( 24 );// extra vertical space for icons, and for box around edit. jTable.setSelectionBackground( BACKGROUND_FOR_SELECTION ); // get information about all columns. final TableColumnModel columnModel = jTable.getColumnModel(); // setting column widths: // state final TableColumn stateCol = columnModel.getColumn( COL_FOR_STATE ); stateCol.setPreferredWidth( COL_WIDTH_FOR_STATE ); stateCol.setMinWidth( COL_WIDTH_FOR_STATE ); stateCol.setMaxWidth( COL_WIDTH_FOR_STATE ); // app name final TableColumn appCol = columnModel.getColumn( COL_FOR_APP ); appCol.setPreferredWidth( COL_WIDTH_FOR_APP ); appCol.setMinWidth( 60 ); appCol.setMaxWidth( COL_WIDTH_FOR_APP + 50 ); // version final TableColumn versionCol = columnModel.getColumn( COL_FOR_VERSION ); versionCol.setPreferredWidth( COL_WIDTH_FOR_VERSION ); versionCol.setMinWidth( 60 ); versionCol.setMaxWidth( COL_WIDTH_FOR_VERSION + 100 ); // date released final TableColumn dateReleasedCol = columnModel.getColumn( COL_FOR_DATE_RELEASED ); dateReleasedCol.setPreferredWidth( COL_WIDTH_FOR_DATE_RELEASED ); dateReleasedCol.setMinWidth( 60 ); dateReleasedCol.setMaxWidth( COL_WIDTH_FOR_DATE_RELEASED ); // description final TableColumn descriptionCol = columnModel.getColumn( COL_FOR_DESCRIPTION ); descriptionCol.setPreferredWidth( COL_WIDTH_FOR_DESCRIPTION ); descriptionCol.setMinWidth( 60 ); descriptionCol.setMaxWidth( COL_WIDTH_FOR_DESCRIPTION + 300 ); // sni final TableColumn sniCol = columnModel.getColumn( COL_FOR_SNI ); sniCol.setPreferredWidth( COL_WIDTH_FOR_SNI ); sniCol.setMinWidth( COL_WIDTH_FOR_SNI ); sniCol.setMaxWidth( COL_WIDTH_FOR_SNI ); // url final TableColumn urlCol = columnModel.getColumn( COL_FOR_URL ); urlCol.setPreferredWidth( COL_WIDTH_FOR_URL ); urlCol.setMinWidth( 60 ); urlCol.setMaxWidth( COL_WIDTH_FOR_URL + 500 ); // marker final TableColumn markerCol = columnModel.getColumn( COL_FOR_MARKER ); markerCol.setPreferredWidth( COL_WIDTH_FOR_MARKER ); markerCol.setMinWidth( 60 ); markerCol.setMaxWidth( COL_WIDTH_FOR_MARKER + 500 ); // adding a bit of extra space between the columns. columnModel.setColumnMargin( COL_MARGIN ); // apply VerCheckHeaderRenderer to all columns. final VerCheckHeaderRenderer verCheckHeaderRenderer = new VerCheckHeaderRenderer(); for ( int i = 0; i < NUMBER_OF_COLS; i++ ) { columnModel.getColumn( i ).setHeaderRenderer( verCheckHeaderRenderer ); } // display just an icon stateCol.setCellRenderer( new AppStateRenderer() ); appCol.setCellRenderer( new RainbowRenderer( FONT_FOR_APP_BODY, FOREGROUND_FOR_APP, JLabel.LEFT ) ); versionCol.setCellRenderer( new RainbowRenderer( FONT_FOR_VERSION, FOREGROUND_FOR_VERSION, JLabel.LEFT ) ); dateReleasedCol.setCellRenderer( new ISODateRenderer( FONT_FOR_DATE_RELEASED, FOREGROUND_FOR_DATE_RELEASED, JLabel.LEFT ) ); descriptionCol.setCellRenderer( new RainbowRenderer( FONT_FOR_DESCRIPTION, FOREGROUND_FOR_DESCRIPTION, JLabel.LEFT ) ); sniCol.setCellRenderer( new RainbowRenderer( FONT_FOR_SNI, FOREGROUND_FOR_SNI, JLabel.LEFT ) ); urlCol.setCellRenderer( new RainbowRenderer( FONT_FOR_URL, FOREGROUND_FOR_URL, JLabel.LEFT ) ); markerCol.setCellRenderer( new RainbowRenderer( FONT_FOR_MARKER, FOREGROUND_FOR_MARKER, JLabel.LEFT ) ); // set up editors. final JTextField scratch = new JTextField(); scratch.setFont( FONT_FOR_EDITING ); scratch.setForeground( FOREGROUND_FOR_EDITING ); scratch.setBackground( BACKGROUND_FOR_EDITING ); final TableCellEditor cellEditor = new DefaultCellEditor( scratch ); appCol.setCellEditor( cellEditor ); versionCol.setCellEditor( cellEditor ); sniCol.setCellEditor( cellEditor ); urlCol.setCellEditor( cellEditor ); markerCol.setCellEditor( cellEditor ); dateReleasedCol.setCellEditor( new ISODateEditor( FOREGROUND_FOR_EDITING, BACKGROUND_FOR_EDITING, FONT_FOR_EDITING, JTextField.LEFT ) ); // allow only one row to be selected at a time. jTable.setSelectionMode( ListSelectionModel.SINGLE_SELECTION ); jTable.setColumnSelectionAllowed( false ); jTable.setRowSelectionAllowed( true ); jTable.setCellSelectionEnabled( true ); // use default setSelectedBackground; } /** * Removes the row with specified appName. Shifts any subsequent rows to the left (subtracts one from their * indices). * * @param appName the name of the app to be removed * * @return the row that was removed from the list * @throws IndexOutOfBoundsException {@inheritDoc} can't happen */ AppToWatch remove( final String appName ) { final int rowIndex = getRowForApp( appName ); return rowIndex >= 0 ? remove( rowIndex ) : null; } /** * abort any edit in progress */ private void stopEdit() { // stop any edit in process TableCellEditor tce = jTable.getCellEditor(); if ( tce != null ) { tce.stopCellEditing(); } } /** * Appends the specified element to the end of this list. * * @param rowData row to be appended to this list * * @return true */ @SuppressWarnings( { "UnusedReturnValue" } ) public boolean add( AppToWatch rowData ) { final int row; final boolean result; final ArrayList frozenAll_Rows = all_Rows; synchronized ( frozenAll_Rows ) { AppToWatch.modifiedTimestamp = System.currentTimeMillis(); stopEdit(); // update the status, relative to current date rowData.normaliseState(); result = frozenAll_Rows.add( rowData ); row = frozenAll_Rows.size() - 1; selectionModel.setSelectionInterval( row, row ); } SwingUtilities.invokeLater( new Runnable() { public void run() { fireTableRowsInserted( row, row ); ensureVisible( row ); } } ); Thread.yield(); return result; } /** * Inserts the specified rowData at the specified position in this list. Shifts the rowData currently at that * position (if any) and any subsequent elements to the right (adds one to their indices). * * @param rowIndex rowIndex at which the specified row is to be inserted * @param rowData rowData to be inserted * * @throws IndexOutOfBoundsException if rowIndex too big */ public void add( final int rowIndex, final AppToWatch rowData ) { final ArrayList frozenAll_Rows = all_Rows; synchronized ( frozenAll_Rows ) { AppToWatch.modifiedTimestamp = System.currentTimeMillis(); stopEdit(); frozenAll_Rows.add( rowIndex, rowData ); selectionModel.setSelectionInterval( rowIndex, rowIndex ); } SwingUtilities.invokeLater( new Runnable() { public void run() { fireTableRowsInserted( rowIndex, rowIndex ); ensureVisible( rowIndex ); } } ); Thread.yield(); } /** * Removes all entries from the model */ @SuppressWarnings( { "UnusedDeclaration" } ) public void clear() { final ArrayList frozenAll_Rows = all_Rows; synchronized ( frozenAll_Rows ) { frozenAll_Rows.clear(); selectionModel.clearSelection(); } SwingUtilities.invokeLater( new Runnable() { public void run() { fireTableDataChanged(); } } ); Thread.yield(); } /** * make sure a given rowIndex is visible * * @param rowIndex 0-based rowIndex to make sure is scrolled into view. */ public void ensureVisible( final int rowIndex ) { SwingUtilities.invokeLater( new Runnable() { public void run() { final Rectangle r; final ArrayList frozenAll_Rows = all_Rows; synchronized ( frozenAll_Rows ) { // automatically handle rowIndex out of range in an appropriate way r = jTable.getCellRect( rowIndex, COL_FOR_STATE/* col */, true ); } if ( r != null ) { jTable.scrollRectToVisible( r ); } } } ); Thread.yield(); } /** * get row * * @param rowIndex desired row. * * @return row date */ @SuppressWarnings( { "UnusedDeclaration" } ) public AppToWatch get( int rowIndex ) { if ( rowIndex < 0 ) { return null; } else { final ArrayList frozenAll_Rows = all_Rows; synchronized ( frozenAll_Rows ) { return frozenAll_Rows.get( rowIndex ); } } } /** * Returns class of column. * * @param columnIndex the column being queried * * @return the Object.class */ public Class getColumnClass( int columnIndex ) { return COLUMN_CLASSES[ columnIndex ]; } /** * how many columns are there? * * @return number of columns. */ public int getColumnCount() { return NUMBER_OF_COLS; } /** * Return columnIndex names * * @param columnIndex the columnIndex being queried * * @return a string containing the default name of columnIndex */ public String getColumnName( int columnIndex ) { return COL_NAMES[ columnIndex ]; } /** * how many rows are there? * * @return number of columns. */ public int getRowCount() { return all_Rows.size(); } /** * search for a row matching a given application name * * @param appName name of the app to look for * * @return row where found, -1 if not found. */ public int getRowForApp( String appName ) { final ArrayList frozenAll_Rows = all_Rows; synchronized ( frozenAll_Rows ) { final int rowCount = frozenAll_Rows.size(); for ( int rowIndex = 0; rowIndex < rowCount; rowIndex++ ) { if ( frozenAll_Rows.get( rowIndex ).getAppName().equalsIgnoreCase( appName ) ) { return rowIndex; } } return -1; } } /** * get the application state of a rowIndex * * @param rowIndex 0-based rowIndex number * * @return Application state */ public AppState getState( final int rowIndex ) { final ArrayList frozenAll_Rows = all_Rows; synchronized ( frozenAll_Rows ) { return frozenAll_Rows.get( rowIndex ).getState(); } } /** * get cell at row/col. * * @param rowIndex 0-based row * @param columnIndex 0-based col * * @return item at row/col. */ public Object getValueAt( int rowIndex, int columnIndex ) { final ArrayList frozenAll_Rows = all_Rows; synchronized ( frozenAll_Rows ) { final AppToWatch rowData = frozenAll_Rows.get( rowIndex ); switch ( columnIndex ) { case COL_FOR_STATE: return rowData.getIcon(); case COL_FOR_APP: return rowData.getAppName(); case COL_FOR_VERSION: return rowData.getVersion(); case COL_FOR_DATE_RELEASED: return rowData.getDateReleased(); case COL_FOR_DESCRIPTION: return rowData.getDescription(); case COL_FOR_SNI: return rowData.isSNIEnabled(); case COL_FOR_URL: return rowData.getVersionURL(); case COL_FOR_MARKER: return rowData.getMarker(); default: return null; } } } /** * Can you edit this cell * * @param rowIndex the row being queried * @param columnIndex the column being queried * * @return false */ public boolean isCellEditable( int rowIndex, int columnIndex ) { return columnIndex != COL_FOR_STATE; } /** * iterator for entire class, just iterates over internal arrayList of rows * * @return Iterator over entire class */ public Iterator iterator() { return all_Rows.iterator(); } /** * Removes the row at the specified position in this list. Shifts any subsequent rows to the left (subtracts one * from their indices). * * @param rowIndex the rowIndex of the row to be removed * * @return the row that was removed from the list * @throws IndexOutOfBoundsException {@inheritDoc} if index out of bounds */ @SuppressWarnings( { "UnusedReturnValue" } ) public AppToWatch remove( final int rowIndex ) { final AppToWatch result; final ArrayList frozenAll_Rows = all_Rows; synchronized ( frozenAll_Rows ) { result = frozenAll_Rows.remove( rowIndex ); // item below pops up to become the new selection if ( rowIndex < frozenAll_Rows.size() ) { selectionModel.setSelectionInterval( rowIndex, rowIndex ); } else { selectionModel.clearSelection(); } } SwingUtilities.invokeLater( new Runnable() { public void run() { fireTableRowsDeleted( rowIndex, rowIndex ); } } ); Thread.yield(); return result; } /** * remove any empty entries. */ public void removeEmpties() { final ArrayList frozenAll_Rows = all_Rows; synchronized ( frozenAll_Rows ) { AppToWatch.modifiedTimestamp = System.currentTimeMillis(); stopEdit(); // must process in reverse order so numbering not jostled for ( int i = frozenAll_Rows.size() - 1; i >= 0; i-- ) { if ( frozenAll_Rows.get( i ).getState() == AppState.EMPTY ) { remove( i );// handles the fire } } } selectionModel.clearSelection(); SwingUtilities.invokeLater( new Runnable() { public void run() { fireTableDataChanged(); } } ); Thread.yield(); } /** * Replaces the rowData at the specified position in this list with the specified rowData. JTable calls its * methods on the Swing thread. VerCheck calls its methods on a background Thread. * * @param rowIndex rowIndex of the element to replace * @param rowData rowData to be stored at the specified position * * @return the row previously at the specified position * @throws IndexOutOfBoundsException {@inheritDoc} if rowIndex out of bounds */ @SuppressWarnings( { "UnusedReturnValue" } ) public AppToWatch set( final int rowIndex, final AppToWatch rowData ) { final AppToWatch result; final ArrayList frozenAll_Rows = all_Rows; synchronized ( frozenAll_Rows ) { AppToWatch.modifiedTimestamp = System.currentTimeMillis(); stopEdit(); result = frozenAll_Rows.set( rowIndex, rowData ); } SwingUtilities.invokeLater( new Runnable() { public void run() { fireTableRowsUpdated( rowIndex, rowIndex ); } } ); Thread.yield(); return result; } /** * set the application state of a rowIndex, triggers visual update of table * * @param rowIndex 0-based rowIndex number * @param state Application state */ public void setState( final int rowIndex, AppState state ) { final ArrayList frozenAll_Rows = all_Rows; synchronized ( frozenAll_Rows ) { frozenAll_Rows.get( rowIndex ).setState( state ); } SwingUtilities.invokeLater( new Runnable() { public void run() { fireTableCellUpdated( rowIndex, COL_FOR_STATE/* col */ ); } } ); Thread.yield(); } /** * This empty implementation is provided so users don't have to implement this method if their data model is not * editable. * * @param aValue value to assign to cell * @param rowIndex row of cell * @param columnIndex column of cell */ public void setValueAt( Object aValue, int rowIndex, int columnIndex ) { final ArrayList frozenAll_Rows = all_Rows; synchronized ( frozenAll_Rows ) { final AppToWatch rowData = frozenAll_Rows.get( rowIndex ); switch ( columnIndex ) { case COL_FOR_STATE: assert false : "attempt to set state via setValueAt"; break; case COL_FOR_APP: rowData.setAppName( ( String ) aValue ); break; case COL_FOR_VERSION: rowData.setVersion( ( String ) aValue ); break; case COL_FOR_DATE_RELEASED: rowData.setDateReleased( ( BigDate ) aValue ); break; case COL_FOR_DESCRIPTION: rowData.setDescription( ( String ) aValue ); break; case COL_FOR_SNI: rowData.setSNI( ( Boolean ) aValue ); break; case COL_FOR_URL: try { rowData.setVersionURL( new URL( ( String ) aValue ) ); } catch ( MalformedURLException e ) { err.println( "invalid URL: " + aValue.toString() ); Audio.BAD_URL.play(); } break; case COL_FOR_MARKER: rowData.setMarker( ( String ) aValue ); break; default: // ignore } } } /** * sort in order by app name */ public void sort() { synchronized ( all_Rows ) { AppToWatch.modifiedTimestamp = System.currentTimeMillis(); stopEdit(); Collections.sort( all_Rows ); selectionModel.clearSelection(); } SwingUtilities.invokeLater( new Runnable() { public void run() { fireTableDataChanged(); } } ); Thread.yield(); } }