content: package com.actelion.research.gui.table;
  
  import com.actelion.research.gui.LookAndFeelHelper;
  import com.actelion.research.gui.hidpi.HiDPIHelper;
  import com.actelion.research.util.ColorHelper;
  
  import javax.swing.*;
  import javax.swing.event.ListSelectionEvent;
  import javax.swing.event.ListSelectionListener;
  import javax.swing.event.TableModelEvent;
  import javax.swing.event.TableModelListener;
  import javax.swing.table.TableColumnModel;
  import javax.swing.table.TableModel;
  import java.awt.*;
  import java.awt.event.MouseEvent;
  import java.awt.event.MouseListener;
  import java.awt.event.MouseMotionListener;
  import java.util.Vector;
  
  public class JTableWithRowNumbers extends JTable implements TableModelListener {
  	protected static Cursor	sResizeCursor,sDefaultCursor;
  
  	private static final long serialVersionUID = 0x20060906;
  
  	private RowNumberPanel	mRowNumberPanel;
  	private JScrollPane		mScrollPane = null;
  	private int				mRowHeaderMinWidth;
  
  	public JTableWithRowNumbers() {
  		super();
  		initialize();
  		}
  
      public JTableWithRowNumbers(int numRows, int numColumns) {
  		super(numRows, numColumns);
  		initialize();
  		}
  
      public JTableWithRowNumbers(final Object[][] rowData, final Object[] columnNames) {
  		super(rowData, columnNames);
  		initialize();
  		}
  
      public JTableWithRowNumbers(TableModel dm) {
  		super(dm);
  		initialize();
  		}
  
      public JTableWithRowNumbers(TableModel dm, TableColumnModel cm) {
  		super(dm, cm);
  		initialize();
  		}
  
      public JTableWithRowNumbers(TableModel dm, TableColumnModel cm, ListSelectionModel sm) {
  		super(dm, cm, sm);
  		initialize();
  		}
  
  	public JTableWithRowNumbers(final Vector rowData, final Vector columnNames) {
  		super(rowData, columnNames);
  		initialize();
  		}
  
  	private void initialize() {
  		mRowNumberPanel = new RowNumberPanel(this);
  
  		setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
  		sDefaultCursor = Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR);
  		sResizeCursor = Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR);
  		}
  
  	public void addHighlightListener(HighlightListener l) {
  		mRowNumberPanel.addHighlightListener(l);
  	}
  
  	public void removeHighlightListener(HighlightListener l) {
  		mRowNumberPanel.removeHighlightListener(l);
  	}
  
  	public int getHighlightedRow() {
  		return mRowNumberPanel.getHighlightedRow();
  		}
  
  	public void setHighlightedRow(int row, boolean informListeners) {
  		mRowNumberPanel.setHighlightedRow(row, informListeners);
  		}
  
  	public JScrollPane getScrollPane() {
  		return mScrollPane;
  		}
  
  	/**
  	 * @return font size of parent table, which includes UI scaling factor
  	 */
  	public int getFontSize() {
  		return getFont().getSize();
  		}
  
  	/**
  	 * @param fontSize including UI scaling factor
  	 */
  	public void setFontSize(int fontSize) {
  		Font font = new Font(Font.SANS_SERIF, Font.PLAIN, fontSize);
  		super.setFont(font);
  		getTableHeader().setFont(font);
  		if (mRowNumberPanel != null) {
  			mRowNumberPanel.setFont(font);
  			validateRowNumberColumnWidth();
  			}
  		}
  
  	/**
  	 * @param height
  	 */
  	@Override
  	public void setRowHeight(int height) {
  		super.setRowHeight(height);
  		if (mRowNumberPanel != null)
  			mRowNumberPanel.repaint();
  		}
  
  	/**
  	 * @param row
  	 * @param height
  	 */
  	@Override
  	public void setRowHeight(int row, int height) {
  		super.setRowHeight(row, height);
  		if (mRowNumberPanel != null)
  			mRowNumberPanel.repaint();
  		}
  
  	@Override
  	public void addNotify() {
  		super.addNotify();
  
  		if (getParent().getParent() instanceof JScrollPane) {
  			mScrollPane = (JScrollPane) getParent().getParent();
  			mScrollPane.setRowHeaderView(mRowNumberPanel);
  			mScrollPane.getVerticalScrollBar().getModel().addChangeListener(e -> mRowNumberPanel.repaint() );
  
  			validateRowNumberColumnWidth();
  
  // make the background of the scrollpane match that of the table.
  //			pane.getViewport().setBackground(getBackground());
  //			pane.getColumnHeader().setBackground(getBackground());
  //			pane.getRowHeader().setBackground(getBackground());
  			}
  		}
  
  	public void setRowHeaderMinWidth(int width) {
  		mRowHeaderMinWidth = width;
  		validateRowNumberColumnWidth();
  		}
  
  	private void validateRowNumberColumnWidth() {
  		if (mScrollPane != null) {
  			JViewport viewport = mScrollPane.getRowHeader();
  			Dimension size = viewport.getPreferredSize();
  			int maxWidth = mRowNumberPanel.getFontMetrics(mRowNumberPanel.getFont()).stringWidth("  "+getRowCount());
  			size.width = Math.max(mRowHeaderMinWidth, maxWidth);
  			viewport.setPreferredSize(size);
  			}
  		}
  
  	@Override
  	public void tableChanged(TableModelEvent e) {
  		super.tableChanged(e);
  
  		if (mRowNumberPanel != null) {
  			validateRowNumberColumnWidth();
  			mRowNumberPanel.invalidate();
  			mRowNumberPanel.repaint();
  			}
  		}
  
  	public void rowNumberClicked(int row) {	// overwrite this if you want specific effects
  		}
  
  	public void setSelectionModel(ListSelectionModel selectionModel) {
  		super.setSelectionModel(selectionModel);
  		}
  	}
  
  	// Inner class used to display the row numbers on the left side of the table. Order
  	// is considered important enough on this screen to re-enforce it with a visual.
  
  class RowNumberPanel extends JPanel implements ListSelectionListener,MouseListener,MouseMotionListener {
  	private static final long serialVersionUID = 0x20060906;
  
  	private static final int cResizeTolerance = 2;
  
  	private final JTableWithRowNumbers mTable;
  	private final Vector<HighlightListener> mHighlightListeners;
  	private boolean mFullRowSelection,mIsResizing;
  	private int mDragStartY,mDragStartRow,mResizingStartRow,mCurrentDragRow,mDragStartRowHeight,mAchorSelectionRow;
  	private int mResizingRowHeight,mHighlightedRow;
  
  	public RowNumberPanel(JTableWithRowNumbers table) {
  		mTable = table;
  		mAchorSelectionRow = -1;
  		mHighlightedRow = -1;
  		mHighlightListeners = new Vector<>();
  		addMouseListener(this);
  		addMouseMotionListener(this);
  		table.getSelectionModel().addListSelectionListener(this);
  	}
  
  	@Override
  	public void setFont(Font font) {
  		super.setFont(font.deriveFont(Font.BOLD));
  	}
  
  	private int rowFromMouseY(MouseEvent e) {
  		return (e.getY() + mTable.getScrollPane().getViewport().getViewRect().y) / mTable.getRowHeight();
  	}
  
  	public void mouseClicked(MouseEvent e) {
  		if (mResizingStartRow == -1)
  			mTable.rowNumberClicked(rowFromMouseY(e));
  	}
  
  	public void mousePressed(MouseEvent e) {
  		mIsResizing = (mResizingStartRow != -1);
  		mFullRowSelection = !mIsResizing;
  		mCurrentDragRow = mDragStartRow = rowFromMouseY(e);
  		if (mFullRowSelection) {
  			if (e.isShiftDown()) {
  				if (mAchorSelectionRow != -1) {
  					mTable.getSelectionModel().setSelectionInterval(mAchorSelectionRow, mCurrentDragRow);
  					mAchorSelectionRow = -1;
  					}
  				else {
  					mTable.getSelectionModel().addSelectionInterval(mCurrentDragRow, mCurrentDragRow);
  					}
  				}
  			else if (e.isControlDown()) {
  				mTable.getSelectionModel().removeSelectionInterval(mCurrentDragRow, mCurrentDragRow);
  				mAchorSelectionRow = -1;
  				}
  			else {
  				mTable.getSelectionModel().setSelectionInterval(mCurrentDragRow, mCurrentDragRow);
  				mAchorSelectionRow = mCurrentDragRow;
  				}
  			repaint();
  			}
  		if (mIsResizing) {
  			mDragStartY = e.getY();
  			mResizingRowHeight = mDragStartRowHeight;
  		}
  	}
  
  	public void mouseReleased(MouseEvent e) {
  		if (mIsResizing) {
  			mIsResizing = false;
  			if (mDragStartRowHeight != mResizingRowHeight) {
  				int scrollValue = mTable.getScrollPane().getVerticalScrollBar().getValue();
  				int newScrollValue = scrollValue + mResizingStartRow * (mResizingRowHeight - mDragStartRowHeight);
  				mTable.setRowHeight(mResizingRowHeight);
  				mTable.getScrollPane().getVerticalScrollBar().setValue(newScrollValue);
  			}
  		}
  	}
  
  	public void mouseEntered(MouseEvent e) {
  	}
  
  	public void mouseExited(MouseEvent e) {
  		if (!mIsResizing)
  			setCursor(JTableWithRowNumbers.sDefaultCursor);
  	}
  
  	public void mouseMoved(MouseEvent e) {
  		Point p = e.getPoint();
  
  		int y = p.y + mTable.getScrollPane().getViewport().getViewRect().y;
  		mDragStartRowHeight = mTable.getRowHeight();
  		int tolerance = HiDPIHelper.scale(cResizeTolerance);
  		int row = y / mDragStartRowHeight;
  		if (row >= mTable.getRowCount())
  			row = -1;
  		int fracY = y % mDragStartRowHeight;
  
  		mResizingStartRow = (row == -1) ? -1 : (fracY <= tolerance && row > 0) ? row - 1 : (fracY >= mDragStartRowHeight - tolerance) ? row : -1;
  		setCursor(mResizingStartRow == -1 ? JTableWithRowNumbers.sDefaultCursor : JTableWithRowNumbers.sResizeCursor);
  
  		if (mHighlightedRow != row) {
  			mHighlightedRow = row;
  			informListeners();
  			repaint();
  		}
  	}
  
  	public void mouseDragged(MouseEvent e) {
  		if (mFullRowSelection) {
  			int row = rowFromMouseY(e);
  			if (mCurrentDragRow != row) {
  				mCurrentDragRow = row;
  				if (e.isShiftDown())
  					mTable.getSelectionModel().addSelectionInterval(mDragStartRow, mCurrentDragRow);
  				else if (e.isControlDown())
  					mTable.getSelectionModel().removeSelectionInterval(mDragStartRow, mCurrentDragRow);
  				else
  					mTable.getSelectionModel().setSelectionInterval(mDragStartRow, mCurrentDragRow);
  				repaint();
  			}
  		}
  		if (mIsResizing) {
  			int rowHeight = Math.min(getHeight()*4/5, Math.max(HiDPIHelper.scale(16), mDragStartRowHeight + e.getY() - mDragStartY));
  			if (mResizingRowHeight != rowHeight) {
  				mResizingRowHeight = rowHeight;
  				mTable.setRowHeight(mResizingStartRow, mResizingRowHeight);
  			}
  			repaint();
  		}
  	}
  
  	public void valueChanged(ListSelectionEvent e) {
  		repaint();
  	}
  
  	public void addHighlightListener(HighlightListener l) {
  		mHighlightListeners.add(l);
  	}
  
  	public void removeHighlightListener(HighlightListener l) {
  		mHighlightListeners.remove(l);
  	}
  
  	public int getHighlightedRow() {
  		return mHighlightedRow;
  		}
  
  	public void setHighlightedRow(int row, boolean informListeners) {
  		mHighlightedRow = row;
  		if (informListeners)
  			informListeners();
  
  		repaint();
  		}
  
  	private void informListeners() {
  		for (HighlightListener l : mHighlightListeners)
  			l.highlightedRowChanged(mHighlightedRow);
  	}
  
  	@Override public void paintComponent(Graphics g) {
  		super.paintComponent(g);
  
  		JScrollPane scrollPane = mTable.getScrollPane();
  		if (scrollPane == null)
  			return;
  
  		Color bg = ColorHelper.darker(UIManager.getColor("Table.background"), 0.75f);
  		Color fg = LookAndFeelHelper.isDarkLookAndFeel() ?
  				ColorHelper.darker(UIManager.getColor("Table.foreground"), 0.8f)
  				: ColorHelper.brighter(UIManager.getColor("Table.foreground"), 0.6f);
  		Color lc = UIManager.getColor("Table.background");
  		Color sbg = ColorHelper.darker(UIManager.getColor("Table.selectionBackground"), 0.75f);
  		Dimension size = getSize();
  
  		Rectangle viewRect = scrollPane.getViewport().getViewRect();
  		int rowAtTop = mTable.rowAtPoint(new Point(0, viewRect.y));
  		int rowAtBottom = mTable.rowAtPoint(new Point(0, viewRect.y + viewRect.height));
  		int firstRow = Math.max(0, rowAtTop);
  		int lastRow = (rowAtBottom == -1) ? mTable.getRowCount()-1 : rowAtBottom;
  		int tableRowHeight = mTable.getRowHeight();
  
  		int y = firstRow * tableRowHeight - viewRect.y;
  		for (int row=firstRow; row<=lastRow; row++) {
  			int rowHeight = (mIsResizing && row == mResizingStartRow) ? mResizingRowHeight : tableRowHeight;
  			String rowHeader = Integer.toString(row+1);
  			g.setColor(mTable.getSelectionModel().isSelectedIndex(row) ? sbg : bg);
  			g.fillRect(0, y, size.width, rowHeight);
  			g.setColor(fg);
  			g.setFont(getFont());
  			int x = (getWidth() - g.getFontMetrics().stringWidth(rowHeader)) / 2;
  			g.drawString(rowHeader, x, y + (rowHeight + getFont().getSize()) / 2);
  			if (row == mHighlightedRow) {
  				g.setColor(Color.BLUE);
  				for (int i=0; i<HiDPIHelper.scale(2); i++)
  					g.drawRect(i, y+i, size.width-(2*i+1), rowHeight-(2*i+1));
  			}
  			else {
  				g.setColor(lc);
  				g.drawLine(0, y+rowHeight-1, size.width-1, y+rowHeight-1);
  			}
  			y += rowHeight;
  		}
  	}
  }
  