Merge remote-tracking branch 'origin/master' into numpy-array-view
[idea/community.git] / python / src / com / jetbrains / python / actions / PyViewArrayAction.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.jetbrains.python.actions;
17
18 import com.intellij.openapi.actionSystem.AnActionEvent;
19 import com.intellij.openapi.editor.Editor;
20 import com.intellij.openapi.editor.impl.DocumentImpl;
21 import com.intellij.openapi.editor.impl.EditorFactoryImpl;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.ui.DialogWrapper;
24 import com.intellij.ui.components.JBScrollPane;
25 import com.intellij.ui.table.JBTable;
26 import com.intellij.util.containers.HashSet;
27 import com.intellij.xdebugger.frame.XFullValueEvaluator;
28 import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreeListener;
29 import com.intellij.xdebugger.impl.ui.tree.actions.XDebuggerTreeActionBase;
30 import com.intellij.xdebugger.impl.ui.tree.nodes.RestorableStateNode;
31 import com.intellij.xdebugger.impl.ui.tree.nodes.XDebuggerTreeNode;
32 import com.intellij.xdebugger.impl.ui.tree.nodes.XValueContainerNode;
33 import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl;
34 import com.jetbrains.python.PythonFileType;
35 import org.jetbrains.annotations.NotNull;
36 import org.jetbrains.annotations.Nullable;
37
38 import javax.swing.*;
39 import javax.swing.event.ChangeEvent;
40 import javax.swing.event.ChangeListener;
41 import javax.swing.event.TableModelEvent;
42 import javax.swing.event.TableModelListener;
43 import javax.swing.table.*;
44 import java.awt.*;
45 import java.awt.event.ActionEvent;
46 import java.awt.event.ItemEvent;
47 import java.awt.event.ItemListener;
48 import java.awt.event.KeyEvent;
49 import java.beans.PropertyChangeEvent;
50 import java.beans.PropertyChangeListener;
51 import java.util.List;
52
53 /**
54  * @author amarch
55  */
56
57 public class PyViewArrayAction extends XDebuggerTreeActionBase {
58
59   @Override
60   protected void perform(XValueNodeImpl node, @NotNull String nodeName, AnActionEvent e) {
61     final MyDialog dialog = new MyDialog(e.getProject(), node, nodeName);
62     dialog.setTitle("View Array");
63     dialog.setValue(node);
64     dialog.show();
65   }
66
67   private abstract class ArrayValueProvider {
68
69     public abstract Object[][] parseValues(String rawValue);
70   }
71
72   private class NumpyArrayValueProvider extends ArrayValueProvider {
73
74     @Override
75     public Object[][] parseValues(String rawValues) {
76       return null;
77     }
78
79     private boolean isNumeric(String value) {
80       return true;
81     }
82   }
83
84
85   private class MyDialog extends DialogWrapper {
86     public JTable myTable;
87     private XValueNodeImpl myNode;
88     private String myNodeName;
89     private Project myProject;
90     private MyComponent myComponent;
91
92     private MyDialog(Project project, XValueNodeImpl node, @NotNull String nodeName) {
93       super(project, false);
94       setModal(false);
95       setCancelButtonText("Close");
96       setCrossClosesWindow(true);
97
98       myNode = node;
99       myNodeName = nodeName;
100       myProject = project;
101
102       myComponent = new MyComponent();
103       myTable = myComponent.getTable();
104
105       init();
106     }
107
108
109     public abstract class XDebuggerTreeTableListener implements XDebuggerTreeListener {
110       abstract void setBaseNode(XValueNodeImpl baseNode);
111
112       abstract XValueContainerNode findItems(@NotNull XDebuggerTreeNode node);
113     }
114
115     public void setValue(XValueNodeImpl node) {
116       final ArrayValueProvider valueProvider;
117
118       if (node.getValuePresentation() != null &&
119           node.getValuePresentation().getType() != null &&
120           node.getValuePresentation().getType().equals("ndarray")) {
121         valueProvider = new NumpyArrayValueProvider();
122
123         //myComponent.setDefaultSpinnerText();
124
125         XDebuggerTreeTableListener tableUpdater = new XDebuggerTreeTableListener() {
126
127           XValueNodeImpl baseNode;
128
129           XValueContainerNode innerNdarray;
130
131           XValueContainerNode innerItems;
132
133           int[] shape;
134
135           int depth = 0;
136
137           int loadedRows = 0;
138
139           HashSet<String> unloadedRowNumbers = new HashSet<String>();
140
141           boolean baseChildrenLoaded = false;
142
143           boolean numeric = false;
144
145           boolean dataLoaded = false;
146
147           Object[][] data;
148
149           @Override
150           public void nodeLoaded(@NotNull RestorableStateNode node, String name) {
151             System.out.printf(name + " node loaded\n");
152
153             if (!baseChildrenLoaded &&
154                 (shape == null || name.equals("dtype")) &&
155                 ((XValueNodeImpl)node.getParent()).getName().equals(baseNode.getName())) {
156               if (name.equals("shape")) {
157                 String rawValue = node.getRawValue();
158                 String[] shapes = rawValue.substring(1, rawValue.length() - 1).split(",");
159                 shape = new int[shapes.length];
160                 for (int i = 0; i < shapes.length; i++) {
161                   shape[i] = Integer.parseInt(shapes[i].trim());
162                 }
163                 depth = Math.max(shape.length - 2, 0);
164               }
165
166               if (name.equals("dtype")) {
167                 String rawValue = node.getRawValue();
168                 if ("biufc".contains(rawValue.substring(0, 1))) {
169                   numeric = true;
170                 }
171               }
172             }
173           }
174
175           @Override
176           public void childrenLoaded(@NotNull XDebuggerTreeNode node, @NotNull List<XValueContainerNode<?>> children, boolean last) {
177             System.out.printf(children + "children loaded\n");
178
179             if (dataLoaded) {
180               return;
181             }
182
183             //todo: not compute children if they yet computed
184
185             if (!baseChildrenLoaded && node.equals(baseNode)) {
186               baseChildrenLoaded = true;
187               innerNdarray = (XValueContainerNode)node;
188               if (shape != null) {
189                 if (shape.length >= 2) {
190                   data = new Object[shape[shape.length - 2]][shape[shape.length - 1]];
191                 }
192                 else {
193                   data = new Object[1][shape[0]];
194                 }
195               }
196             }
197
198             //go deeper
199             if (depth > 0) {
200               if (innerNdarray != null && innerNdarray.equals(node)) {
201                 innerNdarray = null;
202                 innerItems = findItems(node);
203                 innerItems.startComputingChildren();
204               }
205
206               if (innerItems != null && innerItems.equals(node)) {
207                 innerNdarray = (XValueContainerNode)node.getChildAt(1);
208                 innerItems = null;
209                 innerNdarray.startComputingChildren();
210                 depth -= 1;
211               }
212
213               return;
214             }
215
216             //find ndarray slice to display
217             if (depth == 0) {
218               innerItems = findItems(node);
219               innerItems.startComputingChildren();
220               depth -= 1;
221               return;
222             }
223
224             if (depth == -1 && node.equals(innerItems)) {
225               if (shape != null && shape.length == 1) {
226                 for (int i = 0; i < node.getChildCount() - 1; i++) {
227                   data[0][i] = fixValue(((XValueNodeImpl)node.getChildAt(i + 1)).getRawValue());
228                 }
229                 loadData();
230                 loadedRows = 1;
231               }
232               else {
233                 for (int i = 0; i < node.getChildCount() - 1; i++) {
234                   ((XValueNodeImpl)node.getChildAt(i + 1)).startComputingChildren();
235                   unloadedRowNumbers.add(((XValueNodeImpl)node.getChildAt(i + 1)).getName());
236                 }
237                 depth -= 1;
238               }
239               return;
240             }
241
242
243             if (depth == -2) {
244               String name = ((XValueNodeImpl)node).getName();
245               // ndarrray children not computed yet
246               if (unloadedRowNumbers.contains(name)) {
247                 unloadedRowNumbers.remove(name);
248                 findItems(node).startComputingChildren();
249                 return;
250               }
251
252               if (name.startsWith("[")) {
253                 int row = parseName(((XValueNodeImpl)node.getParent()).getName());
254                 if (data[row][0] == null) {
255                   for (int i = 0; i < node.getChildCount() - 1; i++) {
256                     data[row][i] = fixValue(((XValueNodeImpl)node.getChildAt(i + 1)).getRawValue());
257                   }
258                   loadedRows += 1;
259                 }
260               }
261             }
262
263             if (loadedRows == shape[shape.length - 2]) {
264               loadData();
265             }
266           }
267
268           //todo: remove this, make correct code in python
269           private String fixValue(String value) {
270             if (!numeric) {
271               return "\'" + value + "\'";
272             }
273             return value;
274           }
275
276           public void setBaseNode(XValueNodeImpl baseNode) {
277             this.baseNode = baseNode;
278           }
279
280           private int parseName(String name) {
281             int open = name.indexOf('(');
282             return Integer.parseInt(name.substring(1, open - 2));
283           }
284
285           XValueContainerNode findItems(@NotNull XDebuggerTreeNode node) {
286             for (int i = 0; i < node.getChildCount(); i++) {
287               if (node.getChildAt(i).toString().startsWith("[")) {
288                 return (XValueContainerNode)node.getChildAt(i);
289               }
290             }
291             return null;
292           }
293
294           private void loadData() {
295
296             DefaultTableModel model = new DefaultTableModel(data, range(0, data[0].length - 1));
297
298             myTable.setModel(model);
299             myTable.setDefaultEditor(myTable.getColumnClass(0), new MyTableCellEditor(myProject));
300
301             if (numeric) {
302               double min = Double.MAX_VALUE;
303               double max = Double.MIN_VALUE;
304               if (data.length > 0) {
305                 try {
306                   for (int i = 0; i < data.length; i++) {
307                     for (int j = 0; j < data[0].length; j++) {
308                       double d = Double.parseDouble(data[i][j].toString());
309                       min = min > d ? d : min;
310                       max = max < d ? d : max;
311                     }
312                   }
313                 }
314                 catch (NumberFormatException e) {
315                   min = 0;
316                   max = 0;
317                 }
318               }
319               else {
320                 min = 0;
321                 max = 0;
322               }
323
324               myTable.setDefaultRenderer(myTable.getColumnClass(0), new MyTableCellRenderer(min, max));
325             }
326             else {
327               myComponent.getColored().setSelected(false);
328               myComponent.getColored().setVisible(false);
329             }
330
331             myComponent.getTextField().setText(getDefaultSliceRepresentation());
332             dataLoaded = true;
333             innerItems = null;
334             innerNdarray = null;
335           }
336
337           public String getDefaultSliceRepresentation() {
338             String representation = "";
339
340             if (baseNode != null) {
341               representation += baseNode.getName();
342               if (shape != null && shape.length > 0) {
343                 for (int i = 0; i < shape.length - 2; i++) {
344                   representation += "[0]";
345                 }
346                 if (shape.length == 1) {
347                   representation += "[0:" + shape[0] + "]";
348                 }
349                 else {
350                   representation += "[0:" + shape[shape.length - 2] + "][0:" + shape[shape.length - 1] + "]";
351                 }
352               }
353             }
354
355             return representation;
356           }
357         };
358
359         tableUpdater.setBaseNode(node);
360
361         node.getTree().addTreeListener(tableUpdater);
362
363         node.startComputingChildren();
364       }
365     }
366
367     private String[] range(int min, int max) {
368       String[] array = new String[max - min + 1];
369       for (int i = min; i <= max; i++) {
370         array[i] = Integer.toString(i);
371       }
372       return array;
373     }
374
375     private String evaluateFullValue(XValueNodeImpl node) {
376       final String[] result = new String[1];
377
378       XFullValueEvaluator.XFullValueEvaluationCallback valueEvaluationCallback = new XFullValueEvaluator.XFullValueEvaluationCallback() {
379         @Override
380         public void evaluated(@NotNull String fullValue) {
381           result[0] = fullValue;
382         }
383
384         @Override
385         public void evaluated(@NotNull String fullValue, @Nullable Font font) {
386           result[0] = fullValue;
387         }
388
389         @Override
390         public void errorOccurred(@NotNull String errorMessage) {
391           result[0] = errorMessage;
392         }
393
394         @Override
395         public boolean isObsolete() {
396           return false;
397         }
398       };
399
400       if (node.getFullValueEvaluator() != null) {
401         node.getFullValueEvaluator().startEvaluation(valueEvaluationCallback);
402       }
403       else {
404         return node.getRawValue();
405       }
406
407       return result[0];
408     }
409
410     @Override
411     @NotNull
412     protected Action[] createActions() {
413       return new Action[]{getCancelAction()};
414     }
415
416     @Override
417     protected String getDimensionServiceKey() {
418       return "#com.jetbrains.python.actions.PyViewArrayAction";
419     }
420
421     @Override
422     protected JComponent createCenterPanel() {
423       return myComponent;
424     }
425   }
426
427   private class MyComponent extends JPanel {
428     private JScrollPane myScrollPane;
429     private JTextField myTextField;
430     private JBTable myTable;
431     private JCheckBox myCheckBox;
432
433     private static final String DATA_LOADING_IN_PROCESS = "Please wait, load array data.";
434
435     public MyComponent() {
436       super(new GridBagLayout());
437
438       myTextField = new JTextField();
439       myTextField.setToolTipText("Format");
440
441       myTable = new JBTable() {
442         public boolean getScrollableTracksViewportWidth() {
443           return getPreferredSize().width < getParent().getWidth();
444         }
445       };
446       myTable.setAutoResizeMode(JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS);
447
448       myCheckBox = new JCheckBox();
449       myCheckBox.setText("Colored");
450       myCheckBox.setSelected(true);
451       myCheckBox.addItemListener(new ItemListener() {
452         @Override
453         public void itemStateChanged(ItemEvent e) {
454           if (e.getSource() == myCheckBox) {
455             if (myTable.getCellRenderer(0, 0) instanceof MyTableCellRenderer) {
456               MyTableCellRenderer renderer = (MyTableCellRenderer)myTable.getCellRenderer(0, 0);
457               if (myCheckBox.isSelected()) {
458                 renderer.setColored(true);
459               }
460               else {
461                 renderer.setColored(false);
462               }
463             }
464             myScrollPane.repaint();
465           }
466         }
467       });
468
469       myScrollPane = new JBScrollPane(myTable);
470       myScrollPane.setHorizontalScrollBarPolicy(JBScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
471       myScrollPane.setVerticalScrollBarPolicy(JBScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
472
473       JTable rowTable = new RowNumberTable(myTable);
474       myScrollPane.setRowHeaderView(rowTable);
475       myScrollPane.setCorner(JScrollPane.UPPER_LEFT_CORNER,
476                              rowTable.getTableHeader());
477
478       add(myScrollPane,
479           new GridBagConstraints(0, 0, 2, 1, 1, 0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
480       add(myTextField,
481           new GridBagConstraints(0, 1, 1, 1, 1, 0, GridBagConstraints.SOUTH, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
482       add(myCheckBox,
483           new GridBagConstraints(1, 1, 1, 1, 1, 0, GridBagConstraints.SOUTH, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
484     }
485
486     public JTextField getTextField() {
487       return myTextField;
488     }
489
490     public JBTable getTable() {
491       return myTable;
492     }
493
494     public JCheckBox getColored() {
495       return myCheckBox;
496     }
497
498     public void setDefaultSpinnerText() {
499       DefaultTableModel model = new DefaultTableModel(1, 1);
500       myTable.setModel(model);
501       myTable.setValueAt(DATA_LOADING_IN_PROCESS, 0, 0);
502     }
503   }
504
505
506   class MyTableCellRenderer extends DefaultTableCellRenderer {
507
508     double min;
509     double max;
510     Color minColor;
511     Color maxColor;
512     boolean colored = true;
513
514     public MyTableCellRenderer(double min, double max) {
515       this.min = min;
516       this.max = max;
517       minColor = new Color(100, 0, 0, 200);
518       maxColor = new Color(254, 0, 0, 200);
519     }
520
521     public void setColored(boolean colored) {
522       this.colored = colored;
523     }
524
525     public Component getTableCellRendererComponent(JTable table, Object value,
526                                                    boolean isSelected, boolean hasFocus, int rowIndex, int vColIndex) {
527       if (isSelected) {
528         // cell (and perhaps other cells) are selected
529       }
530
531       if (hasFocus) {
532         // this cell is the anchor and the table has the focus
533       }
534
535       if (value != null) {
536         setText(value.toString());
537       }
538
539
540       if (max != min) {
541         if (colored) {
542           try {
543             double med = Double.parseDouble(value.toString());
544             int r = (int)(minColor.getRed() + Math.round((maxColor.getRed() - minColor.getRed()) / (max - min) * (med - min)));
545             this.setBackground(new Color(r % 256, 0, 0, 200));
546           }
547           catch (NumberFormatException e) {
548           }
549         }
550         else {
551           this.setBackground(new Color(255, 255, 255));
552         }
553       }
554
555
556       return this;
557     }
558   }
559
560
561   class MyTableCellEditor extends AbstractCellEditor implements TableCellEditor {
562     Editor myEditor;
563     Project myProject;
564
565     public MyTableCellEditor(Project project) {
566       super();
567       myProject = project;
568     }
569
570     public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected,
571                                                  int rowIndex, int vColIndex) {
572
573
574       //PyExpressionCodeFragmentImpl fragment = new PyExpressionCodeFragmentImpl(myProject, "array_view.py", value.toString(), true);
575       //
576       //myEditor = EditorFactoryImpl.getInstance().
577       //  createEditor(PsiDocumentManager.getInstance(myProject).getDocument(fragment), myProject);
578
579
580       myEditor =
581         EditorFactoryImpl.getInstance().createEditor(new DocumentImpl(value.toString()), myProject, PythonFileType.INSTANCE, false);
582
583
584       JComponent editorComponent = myEditor.getContentComponent();
585
586       editorComponent.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
587         .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "enterStroke");
588       editorComponent.getActionMap().put("enterStroke", new AbstractAction() {
589         @Override
590         public void actionPerformed(ActionEvent e) {
591           doOKAction();
592         }
593       });
594       editorComponent.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
595         .put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "escapeStroke");
596       editorComponent.getActionMap().put("escapeStroke", new AbstractAction() {
597         @Override
598         public void actionPerformed(ActionEvent e) {
599           cancelEditing();
600         }
601       });
602
603       return editorComponent;
604     }
605
606     public Object getCellEditorValue() {
607       return myEditor.getDocument().getText();
608     }
609
610     public void doOKAction() {
611       //todo: not performed
612       System.out.println("ok");
613     }
614
615     public void cancelEditing() {
616       System.out.println("esc");
617     }
618   }
619
620   /*
621  *      Use a JTable as a renderer for row numbers of a given main table.
622  *  This table must be added to the row header of the scrollpane that
623  *  contains the main table.
624  */
625   public class RowNumberTable extends JTable
626     implements ChangeListener, PropertyChangeListener, TableModelListener {
627     private JTable main;
628
629     public RowNumberTable(JTable table) {
630       main = table;
631       main.addPropertyChangeListener(this);
632       main.getModel().addTableModelListener(this);
633
634       setFocusable(false);
635       setAutoCreateColumnsFromModel(false);
636       setSelectionModel(main.getSelectionModel());
637
638
639       TableColumn column = new TableColumn();
640       column.setHeaderValue(" ");
641       addColumn(column);
642       column.setCellRenderer(new RowNumberRenderer());
643
644       getColumnModel().getColumn(0).setPreferredWidth(50);
645       setPreferredScrollableViewportSize(getPreferredSize());
646     }
647
648     @Override
649     public void addNotify() {
650       super.addNotify();
651
652       Component c = getParent();
653
654       //  Keep scrolling of the row table in sync with the main table.
655
656       if (c instanceof JViewport) {
657         JViewport viewport = (JViewport)c;
658         viewport.addChangeListener(this);
659       }
660     }
661
662     /*
663      *  Delegate method to main table
664      */
665     @Override
666     public int getRowCount() {
667       return main.getRowCount();
668     }
669
670     @Override
671     public int getRowHeight(int row) {
672       int rowHeight = main.getRowHeight(row);
673
674       if (rowHeight != super.getRowHeight(row)) {
675         super.setRowHeight(row, rowHeight);
676       }
677
678       return rowHeight;
679     }
680
681     /*
682      *  No model is being used for this table so just use the row number
683      *  as the value of the cell.
684      */
685     @Override
686     public Object getValueAt(int row, int column) {
687       return Integer.toString(row + 1);
688     }
689
690     /*
691      *  Don't edit data in the main TableModel by mistake
692      */
693     @Override
694     public boolean isCellEditable(int row, int column) {
695       return false;
696     }
697
698     /*
699      *  Do nothing since the table ignores the model
700      */
701     @Override
702     public void setValueAt(Object value, int row, int column) {
703     }
704
705     //
706     //  Implement the ChangeListener
707     //
708     public void stateChanged(ChangeEvent e) {
709       //  Keep the scrolling of the row table in sync with main table
710
711       JViewport viewport = (JViewport)e.getSource();
712       JScrollPane scrollPane = (JScrollPane)viewport.getParent();
713       scrollPane.getVerticalScrollBar().setValue(viewport.getViewPosition().y);
714     }
715
716     //
717     //  Implement the PropertyChangeListener
718     //
719     public void propertyChange(PropertyChangeEvent e) {
720       //  Keep the row table in sync with the main table
721
722       if ("selectionModel".equals(e.getPropertyName())) {
723         setSelectionModel(main.getSelectionModel());
724       }
725
726       if ("rowHeight".equals(e.getPropertyName())) {
727         repaint();
728       }
729
730       if ("model".equals(e.getPropertyName())) {
731         main.getModel().addTableModelListener(this);
732         revalidate();
733       }
734     }
735
736     //
737     //  Implement the TableModelListener
738     //
739     @Override
740     public void tableChanged(TableModelEvent e) {
741       revalidate();
742     }
743
744     /*
745      *  Attempt to mimic the table header renderer
746      */
747     private class RowNumberRenderer extends DefaultTableCellRenderer {
748       public RowNumberRenderer() {
749         setHorizontalAlignment(JLabel.CENTER);
750       }
751
752       public Component getTableCellRendererComponent(
753         JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
754         if (table != null) {
755           JTableHeader header = table.getTableHeader();
756
757           if (header != null) {
758             setForeground(header.getForeground());
759             setBackground(header.getBackground());
760             setFont(header.getFont());
761           }
762         }
763
764         if (isSelected) {
765           setFont(getFont().deriveFont(Font.BOLD));
766         }
767
768         setText((value == null) ? "" : value.toString());
769         setBorder(UIManager.getBorder("TableHeader.cellBorder"));
770
771         return this;
772       }
773     }
774   }
775 }