NPE fix
[idea/community.git] / platform / platform-api / src / com / intellij / ui / table / JBTable.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.ui.table;
17
18 import com.intellij.Patches;
19 import com.intellij.openapi.util.Disposer;
20 import com.intellij.openapi.wm.IdeFocusManager;
21 import com.intellij.ui.*;
22 import com.intellij.ui.components.JBViewport;
23 import com.intellij.ui.speedSearch.SpeedSearchSupply;
24 import com.intellij.util.ui.*;
25 import org.jetbrains.annotations.NotNull;
26
27 import javax.swing.*;
28 import javax.swing.event.*;
29 import javax.swing.table.*;
30 import java.awt.*;
31 import java.awt.event.KeyEvent;
32 import java.awt.event.MouseAdapter;
33 import java.awt.event.MouseEvent;
34 import java.beans.PropertyChangeEvent;
35 import java.beans.PropertyChangeListener;
36 import java.util.Arrays;
37 import java.util.Comparator;
38 import java.util.EventObject;
39
40 public class JBTable extends JTable implements ComponentWithEmptyText, ComponentWithExpandableItems<TableCell> {
41   public static final int PREFERRED_SCROLLABLE_VIEWPORT_HEIGHT_IN_ROWS = 7;
42
43   private final StatusText myEmptyText;
44   private final ExpandableItemsHandler<TableCell> myExpandableItemsHandler;
45
46   private MyCellEditorRemover myEditorRemover;
47   private boolean myEnableAntialiasing;
48
49   private int myRowHeight = -1;
50   private boolean myRowHeightIsExplicitlySet;
51   private boolean myRowHeightIsComputing;
52
53   private Integer myMinRowHeight;
54   private boolean myStriped;
55
56   private AsyncProcessIcon myBusyIcon;
57   private boolean myBusy;
58
59
60   public JBTable() {
61     this(new DefaultTableModel());
62   }
63
64   public JBTable(TableModel model) {
65     this(model, null);
66   }
67
68   public JBTable(final TableModel model, final TableColumnModel columnModel) {
69     super(model, columnModel);
70
71     setSurrendersFocusOnKeystroke(true);
72
73     myEmptyText = new StatusText(this) {
74       @Override
75       protected boolean isStatusVisible() {
76         return isEmpty();
77       }
78     };
79
80     myExpandableItemsHandler = ExpandableItemsHandlerFactory.install(this);
81
82     setFillsViewportHeight(true);
83
84     addMouseListener(new MyMouseListener());
85     getColumnModel().addColumnModelListener(new TableColumnModelListener() {
86       @Override
87       public void columnMarginChanged(ChangeEvent e) {
88         if (cellEditor != null && !(cellEditor instanceof Animated)) {
89           cellEditor.stopCellEditing();
90         }
91       }
92
93       @Override
94       public void columnSelectionChanged(@NotNull ListSelectionEvent e) {
95       }
96
97       @Override
98       public void columnAdded(@NotNull TableColumnModelEvent e) {
99       }
100
101       @Override
102       public void columnMoved(@NotNull TableColumnModelEvent e) {
103       }
104
105       @Override
106       public void columnRemoved(@NotNull TableColumnModelEvent e) {
107       }
108     });
109
110     final TableModelListener modelListener = new TableModelListener() {
111       @Override
112       public void tableChanged(@NotNull final TableModelEvent e) {
113         if (!myRowHeightIsExplicitlySet) {
114           myRowHeight = -1;
115         }
116         if (e.getType() == TableModelEvent.DELETE && isEmpty() || e.getType() == TableModelEvent.INSERT && !isEmpty()) {
117           repaintViewport();
118         }
119       }
120     };
121
122     if (getModel() != null) getModel().addTableModelListener(modelListener);
123     addPropertyChangeListener("model", new PropertyChangeListener() {
124       @Override
125       public void propertyChange(@NotNull PropertyChangeEvent evt) {
126         repaintViewport();
127
128         if (evt.getOldValue() instanceof TableModel) {
129           ((TableModel)evt.getOldValue()).removeTableModelListener(modelListener);
130         }
131         if (evt.getNewValue() instanceof TableModel) {
132           ((TableModel)evt.getNewValue()).addTableModelListener(modelListener);
133         }
134       }
135     });
136
137
138     //noinspection UnusedDeclaration
139     boolean marker = Patches.SUN_BUG_ID_4503845; // Don't remove. It's a marker for find usages
140   }
141
142   @Override
143   public int getRowHeight() {
144     if (myRowHeightIsComputing) {
145       return super.getRowHeight();
146     }
147
148     if (myRowHeight < 0) {
149       try {
150         myRowHeightIsComputing = true;
151         for (int row = 0; row < getRowCount(); row++) {
152           for (int column = 0; column < getColumnCount(); column++) {
153             final TableCellRenderer renderer = getCellRenderer(row, column);
154             if (renderer != null) {
155               final Object value = getValueAt(row, column);
156               final Component component = renderer.getTableCellRendererComponent(this, value, true, true, row, column);
157               if (component != null) {
158                 final Dimension size = component.getPreferredSize();
159                 myRowHeight = Math.max(size.height, myRowHeight);
160               }
161             }
162           }
163         }
164       }
165       finally {
166         myRowHeightIsComputing = false;
167       }
168     }
169
170     if (myMinRowHeight == null) {
171       myMinRowHeight = getFontMetrics(UIManager.getFont("Label.font")).getHeight();
172     }
173
174     return Math.max(myRowHeight, myMinRowHeight);
175   }
176
177   public void setShowColumns(boolean value) {
178     JTableHeader tableHeader = getTableHeader();
179     tableHeader.setVisible(value);
180     tableHeader.setPreferredSize(value ? null : new Dimension());
181   }
182
183   @Override
184   public void setRowHeight(int rowHeight) {
185     myRowHeight = rowHeight;
186     myRowHeightIsExplicitlySet = true;
187     // call super to clean rowModel
188     super.setRowHeight(rowHeight);
189   }
190
191   @Override
192   public void updateUI() {
193     super.updateUI();
194     myMinRowHeight = null;
195   }
196
197   private void repaintViewport() {
198     if (!isDisplayable() || !isVisible()) return;
199
200     Container p = getParent();
201     if (p instanceof JBViewport) {
202       p.repaint();
203     }
204   }
205
206   @NotNull
207   @Override
208   protected JTableHeader createDefaultTableHeader() {
209     return new JBTableHeader();
210   }
211
212   public boolean isEmpty() {
213     return getRowCount() == 0;
214   }
215
216   @Override
217   public void setModel(@NotNull TableModel model) {
218     super.setModel(model);
219
220     if (model instanceof SortableColumnModel) {
221       final SortableColumnModel sortableModel = (SortableColumnModel)model;
222       if (sortableModel.isSortable()) {
223         final TableRowSorter<TableModel> rowSorter = createRowSorter(model);
224         rowSorter.setSortsOnUpdates(isSortOnUpdates());
225         setRowSorter(rowSorter);
226         final RowSorter.SortKey sortKey = sortableModel.getDefaultSortKey();
227         if (sortKey != null && sortKey.getColumn() >= 0 && sortKey.getColumn() < model.getColumnCount()) {
228           if (sortableModel.getColumnInfos()[sortKey.getColumn()].isSortable()) {
229             rowSorter.setSortKeys(Arrays.asList(sortKey));
230           }
231         }
232       }
233       else {
234         final RowSorter<? extends TableModel> rowSorter = getRowSorter();
235         if (rowSorter instanceof DefaultColumnInfoBasedRowSorter) {
236           setRowSorter(null);
237         }
238       }
239     }
240   }
241
242   protected boolean isSortOnUpdates() {
243     return true;
244   }
245
246   @Override
247   protected void paintComponent(@NotNull Graphics g) {
248     if (myEnableAntialiasing) {
249       GraphicsUtil.setupAntialiasing(g);
250     }
251     super.paintComponent(g);
252     myEmptyText.paint(this, g);
253   }
254
255   @Override
256   protected void paintChildren(Graphics g) {
257     if (myEnableAntialiasing) {
258       GraphicsUtil.setupAntialiasing(g);
259     }
260     super.paintChildren(g);
261   }
262
263   public void setEnableAntialiasing(boolean flag) {
264     myEnableAntialiasing = flag;
265   }
266
267   public static DefaultCellEditor createBooleanEditor() {
268     return new DefaultCellEditor(new JCheckBox()) {
269       {
270         ((JCheckBox)getComponent()).setHorizontalAlignment(SwingConstants.CENTER);
271       }
272
273       @Override
274       public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
275         Component component = super.getTableCellEditorComponent(table, value, isSelected, row, column);
276         component.setBackground(isSelected ? table.getSelectionBackground() : table.getBackground());
277         return component;
278       }
279     };
280   }
281
282   public void resetDefaultFocusTraversalKeys() {
283     KeyboardFocusManager m = KeyboardFocusManager.getCurrentKeyboardFocusManager();
284     for (Integer each : Arrays.asList(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,
285                                       KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS,
286                                       KeyboardFocusManager.UP_CYCLE_TRAVERSAL_KEYS,
287                                       KeyboardFocusManager.DOWN_CYCLE_TRAVERSAL_KEYS)) {
288       setFocusTraversalKeys(each, m.getDefaultFocusTraversalKeys(each));
289     }
290   }
291
292   @NotNull
293   @Override
294   public StatusText getEmptyText() {
295     return myEmptyText;
296   }
297
298   @Override
299   @NotNull
300   public ExpandableItemsHandler<TableCell> getExpandableItemsHandler() {
301     return myExpandableItemsHandler;
302   }
303
304   @Override
305   public void setExpandableItemsEnabled(boolean enabled) {
306     myExpandableItemsHandler.setEnabled(enabled);
307   }
308
309   @Override
310   public void removeNotify() {
311     if (ScreenUtil.isStandardAddRemoveNotify(this)) {
312       final KeyboardFocusManager keyboardFocusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
313       //noinspection HardCodedStringLiteral
314       keyboardFocusManager.removePropertyChangeListener("permanentFocusOwner", myEditorRemover);
315       //noinspection HardCodedStringLiteral
316       keyboardFocusManager.removePropertyChangeListener("focusOwner", myEditorRemover);
317       super.removeNotify();
318       if (myBusyIcon != null) {
319         remove(myBusyIcon);
320         Disposer.dispose(myBusyIcon);
321         myBusyIcon = null;
322       }
323     }
324     else {
325       super.removeNotify();
326     }
327   }
328
329   @Override
330   public int getScrollableUnitIncrement(@NotNull Rectangle visibleRect, int orientation, int direction) {
331     if (orientation == SwingConstants.VERTICAL) {
332       return super.getScrollableUnitIncrement(visibleRect, orientation, direction);
333     }
334     else { // if orientation == SwingConstants.HORIZONTAL
335       // use smooth editor-like scrolling
336       return SwingUtilities.computeStringWidth(getFontMetrics(getFont()), " ");
337     }
338   }
339
340   @Override
341   public void doLayout() {
342     super.doLayout();
343     if (myBusyIcon != null) {
344       myBusyIcon.updateLocation(this);
345     }
346   }
347
348   @Override
349   public void paint(@NotNull Graphics g) {
350     if (!isEnabled()) {
351       g = new TableGrayer((Graphics2D)g);
352     }
353     super.paint(g);
354     if (myBusyIcon != null) {
355       myBusyIcon.updateLocation(this);
356     }
357   }
358
359   public void setPaintBusy(boolean paintBusy) {
360     if (myBusy == paintBusy) return;
361
362     myBusy = paintBusy;
363     updateBusy();
364   }
365
366   private void updateBusy() {
367     if (myBusy) {
368       if (myBusyIcon == null) {
369         myBusyIcon = new AsyncProcessIcon(toString()).setUseMask(false);
370         myBusyIcon.setOpaque(false);
371         myBusyIcon.setPaintPassiveIcon(false);
372         add(myBusyIcon);
373       }
374     }
375
376     if (myBusyIcon != null) {
377       if (myBusy) {
378         myBusyIcon.resume();
379       }
380       else {
381         myBusyIcon.suspend();
382         //noinspection SSBasedInspection
383         SwingUtilities.invokeLater(new Runnable() {
384           @Override
385           public void run() {
386             if (myBusyIcon != null) {
387               repaint();
388             }
389           }
390         });
391       }
392       if (myBusyIcon != null) {
393         myBusyIcon.updateLocation(this);
394       }
395     }
396   }
397
398   public boolean isStriped() {
399     return myStriped;
400   }
401
402   public void setStriped(boolean striped) {
403     myStriped = striped;
404     if (striped) {
405       getColumnModel().setColumnMargin(0);
406       setIntercellSpacing(new Dimension(getIntercellSpacing().width, 0));
407       setShowGrid(false);
408     }
409   }
410
411   @Override
412   public boolean editCellAt(final int row, final int column, final EventObject e) {
413     if (cellEditor != null && !cellEditor.stopCellEditing()) {
414       return false;
415     }
416
417     if (row < 0 || row >= getRowCount() || column < 0 || column >= getColumnCount()) {
418       return false;
419     }
420
421     if (!isCellEditable(row, column)) {
422       return false;
423     }
424
425     if (e instanceof KeyEvent) {
426       // do not start editing in autoStartsEdit mode on Ctrl-Z and other non-typed events
427       if (!UIUtil.isReallyTypedEvent((KeyEvent)e) || ((KeyEvent)e).getKeyChar() == KeyEvent.CHAR_UNDEFINED) return false;
428
429       SpeedSearchSupply supply = SpeedSearchSupply.getSupply(this);
430       if (supply != null && supply.isPopupActive()) {
431         return false;
432       }
433     }
434
435     if (myEditorRemover == null) {
436       final KeyboardFocusManager keyboardFocusManager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
437       myEditorRemover = new MyCellEditorRemover();
438       //noinspection HardCodedStringLiteral
439       keyboardFocusManager.addPropertyChangeListener("focusOwner", myEditorRemover);
440       //noinspection HardCodedStringLiteral
441       keyboardFocusManager.addPropertyChangeListener("permanentFocusOwner", myEditorRemover);
442     }
443
444     final TableCellEditor editor = getCellEditor(row, column);
445     if (editor != null && editor.isCellEditable(e)) {
446       editorComp = prepareEditor(editor, row, column);
447       //((JComponent)editorComp).setBorder(null);
448       if (editorComp == null) {
449         removeEditor();
450         return false;
451       }
452       editorComp.setBounds(getCellRect(row, column, false));
453       add(editorComp);
454       editorComp.validate();
455
456       if (surrendersFocusOnKeyStroke()) {
457         // this replaces focus request in JTable.processKeyBinding
458         final IdeFocusManager focusManager = IdeFocusManager.findInstanceByComponent(this);
459         focusManager.setTypeaheadEnabled(false);
460         focusManager.requestFocus(editorComp, true).doWhenProcessed(new Runnable() {
461           @Override
462           public void run() {
463             focusManager.setTypeaheadEnabled(true);
464           }
465         });
466       }
467
468       setCellEditor(editor);
469       setEditingRow(row);
470       setEditingColumn(column);
471       editor.addCellEditorListener(this);
472
473       return true;
474     }
475     return false;
476   }
477
478   /**
479    * Always returns false.
480    * If you're interested in value of JTable.surrendersFocusOnKeystroke property, call JBTable.surrendersFocusOnKeyStroke()
481    * @return false
482    * @see #surrendersFocusOnKeyStroke
483    */
484   @Override
485   public boolean getSurrendersFocusOnKeystroke() {
486     return false; // prevents JTable.processKeyBinding from requesting editor component to be focused
487   }
488
489   public boolean surrendersFocusOnKeyStroke() {
490     return super.getSurrendersFocusOnKeystroke();
491   }
492
493   private static boolean isTableDecorationSupported() {
494     return UIUtil.isUnderAlloyLookAndFeel()
495            || UIUtil.isUnderNativeMacLookAndFeel()
496            || UIUtil.isUnderDarcula()
497            || UIUtil.isUnderIntelliJLaF()
498            || UIUtil.isUnderNimbusLookAndFeel()
499            || UIUtil.isUnderWindowsLookAndFeel();
500   }
501
502   @NotNull
503   @Override
504   public Component prepareRenderer(@NotNull TableCellRenderer renderer, int row, int column) {
505     Component result = super.prepareRenderer(renderer, row, column);
506
507     // Fix GTK background
508     if (UIUtil.isUnderGTKLookAndFeel()) {
509       UIUtil.changeBackGround(this, UIUtil.getTreeTextBackground());
510     }
511
512     if (isTableDecorationSupported() && isStriped() && result instanceof JComponent) {
513       final Color bg = row % 2 == 1 ? getBackground() : UIUtil.getDecoratedRowColor();
514       final JComponent c = (JComponent)result;
515       final boolean cellSelected = isCellSelected(row, column);
516       if (!cellSelected) {
517         c.setOpaque(true);
518         c.setBackground(bg);
519         for (Component child : c.getComponents()) {
520           child.setBackground(bg);
521         }
522       }
523     }
524
525     if (myExpandableItemsHandler.getExpandedItems().contains(new TableCell(row, column))) {
526       result = new ExpandedItemRendererComponentWrapper(result);
527     }
528     return result;
529   }
530
531   private final class MyCellEditorRemover implements PropertyChangeListener {
532     private final IdeFocusManager myFocusManager;
533
534     public MyCellEditorRemover() {
535       myFocusManager = IdeFocusManager.findInstanceByComponent(JBTable.this);
536     }
537
538     @Override
539     public void propertyChange(@NotNull final PropertyChangeEvent e) {
540       if (!isEditing()) {
541         return;
542       }
543
544       myFocusManager.doWhenFocusSettlesDown(new Runnable() {
545         @Override
546         public void run() {
547           if (!isEditing()) {
548             return;
549           }
550           Component c = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner();
551           while (c != null) {
552             if (c instanceof JPopupMenu) {
553               c = ((JPopupMenu)c).getInvoker();
554             }
555             if (c == JBTable.this) {
556               // focus remains inside the table
557               return;
558             }
559             else if (c instanceof Window) {
560               if (c == SwingUtilities.getWindowAncestor(JBTable.this)) {
561                 getCellEditor().stopCellEditing();
562               }
563               break;
564             }
565             c = c.getParent();
566           }
567         }
568       });
569     }
570   }
571
572   private final class MyMouseListener extends MouseAdapter {
573     @Override
574     public void mousePressed(@NotNull final MouseEvent e) {
575       if (SwingUtilities.isRightMouseButton(e)) {
576         final int[] selectedRows = getSelectedRows();
577         if (selectedRows.length < 2) {
578           final int row = rowAtPoint(e.getPoint());
579           if (row != -1) {
580             getSelectionModel().setSelectionInterval(row, row);
581           }
582         }
583       }
584     }
585   }
586
587   @SuppressWarnings({"MethodMayBeStatic", "unchecked"})
588   protected TableRowSorter<TableModel> createRowSorter(final TableModel model) {
589     return new DefaultColumnInfoBasedRowSorter(model);
590   }
591
592   protected static class DefaultColumnInfoBasedRowSorter extends TableRowSorter<TableModel> {
593     public DefaultColumnInfoBasedRowSorter(final TableModel model) {
594       super(model);
595       setModelWrapper(new TableRowSorterModelWrapper(model));
596       setMaxSortKeys(1);
597     }
598
599     @Override
600     public Comparator<?> getComparator(final int column) {
601       final TableModel model = getModel();
602       if (model instanceof SortableColumnModel) {
603         final ColumnInfo[] columnInfos = ((SortableColumnModel)model).getColumnInfos();
604         if (column >= 0 && column < columnInfos.length) {
605           final Comparator comparator = columnInfos[column].getComparator();
606           if (comparator != null) return comparator;
607         }
608       }
609
610       return super.getComparator(column);
611     }
612
613     @Override
614     protected boolean useToString(int column) {
615       return false;
616     }
617
618     @Override
619     public boolean isSortable(final int column) {
620       final TableModel model = getModel();
621       if (model instanceof SortableColumnModel) {
622         final ColumnInfo[] columnInfos = ((SortableColumnModel)model).getColumnInfos();
623         if (column >= 0 && column < columnInfos.length) {
624           return columnInfos[column].isSortable() && columnInfos[column].getComparator() != null;
625         }
626       }
627
628       return false;
629     }
630
631     private class TableRowSorterModelWrapper extends ModelWrapper<TableModel, Integer> {
632       private final TableModel myModel;
633
634       private TableRowSorterModelWrapper(@NotNull TableModel model) {
635         myModel = model;
636       }
637
638       @Override
639       public TableModel getModel() {
640         return myModel;
641       }
642
643       @Override
644       public int getColumnCount() {
645         return myModel.getColumnCount();
646       }
647
648       @Override
649       public int getRowCount() {
650         return myModel.getRowCount();
651       }
652
653       @Override
654       public Object getValueAt(int row, int column) {
655         if (myModel instanceof SortableColumnModel) {
656           return ((SortableColumnModel)myModel).getRowValue(row);
657         }
658
659         return myModel.getValueAt(row, column);
660       }
661
662       @NotNull
663       @Override
664       public String getStringValueAt(int row, int column) {
665         TableStringConverter converter = getStringConverter();
666         if (converter != null) {
667           // Use the converter
668           String value = converter.toString(
669             myModel, row, column);
670           if (value != null) {
671             return value;
672           }
673           return "";
674         }
675
676         // No converter, use getValueAt followed by toString
677         Object o = getValueAt(row, column);
678         if (o == null) {
679           return "";
680         }
681         String string = o.toString();
682         if (string == null) {
683           return "";
684         }
685         return string;
686       }
687
688       @Override
689       public Integer getIdentifier(int index) {
690         return index;
691       }
692     }
693   }
694
695   protected class JBTableHeader extends JTableHeader {
696     public JBTableHeader() {
697       super(JBTable.this.columnModel);
698       JBTable.this.addPropertyChangeListener(new PropertyChangeListener() {
699         @Override
700         public void propertyChange(@NotNull PropertyChangeEvent evt) {
701           JBTableHeader.this.revalidate();
702           JBTableHeader.this.repaint();
703         }
704       });
705     }
706
707     @Override
708     public void paint(@NotNull Graphics g) {
709       if (myEnableAntialiasing) {
710         GraphicsUtil.setupAntialiasing(g);
711       }
712       if (!JBTable.this.isEnabled()) {
713         g = new TableGrayer((Graphics2D)g);
714       }
715       super.paint(g);
716     }
717
718     @Override
719     public String getToolTipText(@NotNull final MouseEvent event) {
720       final TableModel model = getModel();
721       if (model instanceof SortableColumnModel) {
722         final int i = columnAtPoint(event.getPoint());
723         final int infoIndex = i >= 0 ? convertColumnIndexToModel(i) : -1;
724         final ColumnInfo[] columnInfos = ((SortableColumnModel)model).getColumnInfos();
725         final String tooltipText = infoIndex >= 0 && infoIndex < columnInfos.length ? columnInfos[infoIndex].getTooltipText() : null;
726         if (tooltipText != null) {
727           return tooltipText;
728         }
729       }
730       return super.getToolTipText(event);
731     }
732   }
733
734   /**
735    * Make it possible to disable a JBTable
736    *
737    * @author Konstantin Bulenkov
738    */
739   private final class TableGrayer extends Graphics2DDelegate {
740     public TableGrayer(Graphics2D g2d) {
741       super(g2d);
742     }
743
744     @Override
745     public void setColor(Color color) {
746       if (color != null && (!UIUtil.isUnderDarcula() || !JBTable.this.getBackground().equals(color))) {
747         //noinspection UseJBColor
748         color = new Color(UIUtil.getGrayFilter().filterRGB(0, 0, color.getRGB()));
749       }
750       super.setColor(color);
751     }
752
753     @NotNull
754     @Override
755     public Graphics create() {
756       return new TableGrayer((Graphics2D)super.create());
757     }
758   }
759 }
760