Fix PY-21429 Duplicate output in Jupyter Notebook
[idea/community.git] / python / ipnb / src / org / jetbrains / plugins / ipnb / editor / panels / code / IpnbCodePanel.java
1 package org.jetbrains.plugins.ipnb.editor.panels.code;
2
3 import com.google.common.collect.Lists;
4 import com.intellij.icons.AllIcons;
5 import com.intellij.openapi.actionSystem.DefaultActionGroup;
6 import com.intellij.openapi.application.Application;
7 import com.intellij.openapi.application.ApplicationManager;
8 import com.intellij.openapi.application.ModalityState;
9 import com.intellij.openapi.editor.Document;
10 import com.intellij.openapi.editor.Editor;
11 import com.intellij.openapi.project.Project;
12 import com.intellij.openapi.ui.VerticalFlowLayout;
13 import com.intellij.openapi.ui.popup.ListPopup;
14 import com.intellij.openapi.util.TextRange;
15 import com.intellij.openapi.util.text.StringUtil;
16 import com.intellij.ui.OnePixelSplitter;
17 import com.intellij.ui.awt.RelativePoint;
18 import com.intellij.util.ui.UIUtil;
19 import org.jetbrains.annotations.NotNull;
20 import org.jetbrains.annotations.Nullable;
21 import org.jetbrains.plugins.ipnb.configuration.IpnbConnectionManager;
22 import org.jetbrains.plugins.ipnb.editor.IpnbEditorUtil;
23 import org.jetbrains.plugins.ipnb.editor.IpnbFileEditor;
24 import org.jetbrains.plugins.ipnb.editor.actions.IpnbHideOutputAction;
25 import org.jetbrains.plugins.ipnb.editor.panels.IpnbEditablePanel;
26 import org.jetbrains.plugins.ipnb.editor.panels.IpnbFilePanel;
27 import org.jetbrains.plugins.ipnb.editor.panels.IpnbPanel;
28 import org.jetbrains.plugins.ipnb.format.cells.IpnbCodeCell;
29 import org.jetbrains.plugins.ipnb.format.cells.output.*;
30
31 import javax.swing.*;
32 import java.awt.*;
33 import java.awt.event.MouseAdapter;
34 import java.awt.event.MouseEvent;
35 import java.util.Arrays;
36 import java.util.List;
37 import java.util.Map;
38
39 public class IpnbCodePanel extends IpnbEditablePanel<JComponent, IpnbCodeCell> {
40   private final Project myProject;
41   @NotNull private final IpnbFileEditor myParent;
42   private final static String COLLAPSED_METADATA = "collapsed";
43   private IpnbCodeSourcePanel myCodeSourcePanel;
44   private final List<IpnbPanel> myOutputPanels = Lists.newArrayList();
45   private HideableOutputPanel myHideableOutputPanel;
46   private boolean mySelectNext;
47
48   public IpnbCodePanel(@NotNull final Project project, @NotNull final IpnbFileEditor parent, @NotNull final IpnbCodeCell cell) {
49     super(cell, new BorderLayout());
50     myProject = project;
51     myParent = parent;
52
53     myViewPanel = createViewPanel();
54     add(myViewPanel);
55     addRightClickMenu();
56   }
57
58   @NotNull
59   public IpnbFileEditor getFileEditor() {
60     return myParent;
61   }
62
63   public Editor getEditor() {
64     return myCodeSourcePanel.getEditor();
65   }
66
67   public void addPromptPanel(@NotNull final JComponent parent, Integer promptNumber,
68                              @NotNull final IpnbEditorUtil.PromptType promptType,
69                              @NotNull final JComponent component) {
70     super.addPromptPanel(parent, promptNumber, promptType, component);
71     if (component instanceof IpnbPanel) {
72       myOutputPanels.add((IpnbPanel)component);
73     }
74   }
75
76   @Override
77   protected JComponent createViewPanel() {
78     final JPanel panel = new JPanel(new VerticalFlowLayout(VerticalFlowLayout.TOP));
79     panel.setBackground(IpnbEditorUtil.getBackground());
80     panel.add(createCodeComponent());
81     myHideableOutputPanel = new HideableOutputPanel();
82     panel.add(myHideableOutputPanel);
83
84     return panel;
85   }
86
87   @Override
88   protected void addRightClickMenu() {
89     myHideableOutputPanel.addMouseListener(new MouseAdapter() {
90       @Override
91       public void mousePressed(MouseEvent e) {
92         if (SwingUtilities.isRightMouseButton(e) && e.getClickCount() == 1) {
93           final ListPopup menu = createPopupMenu(new DefaultActionGroup(new IpnbHideOutputAction(IpnbCodePanel.this)));
94           menu.show(RelativePoint.fromScreen(e.getLocationOnScreen()));
95         }
96       }
97     });
98   }
99
100   class HideableOutputPanel extends OnePixelSplitter{
101     final JPanel myToggleBar;
102     final JPanel myOutputComponent;
103
104     public HideableOutputPanel() {
105       super(true);
106       myToggleBar = createToggleBar(this);
107       myOutputComponent = createOutputPanel();
108
109       final Map<String, Object> metadata = myCell.getMetadata();
110       if (metadata.containsKey(COLLAPSED_METADATA)) {
111         final boolean isCollapsed = (Boolean)metadata.get(COLLAPSED_METADATA);
112         if (isCollapsed && !myCell.getCellOutputs().isEmpty()) {
113           setFirstComponent(myToggleBar);
114           return;
115         }
116       }
117       setSecondComponent(myOutputComponent);
118     }
119
120     public void hideOutputPanel() {
121       setOutputStateInCell(true);
122       setFirstComponent(myToggleBar);
123       setSecondComponent(null);
124     }
125   }
126
127   @NotNull
128   private JPanel createCodeComponent() {
129     myCodeSourcePanel = new IpnbCodeSourcePanel(myProject, this, myCell);
130     final JPanel panel = new JPanel(new GridBagLayout());
131     panel.setBackground(IpnbEditorUtil.getBackground());
132     addPromptPanel(panel, myCell.getPromptNumber(), IpnbEditorUtil.PromptType.In, myCodeSourcePanel);
133
134     final JPanel topComponent = new JPanel(new BorderLayout());
135     topComponent.add(panel, BorderLayout.PAGE_START);
136     return topComponent;
137   }
138
139   @NotNull
140   private JPanel createOutputPanel() {
141     final JPanel outputPanel = new JPanel(new VerticalFlowLayout(VerticalFlowLayout.TOP, true, false));
142     outputPanel.setBackground(IpnbEditorUtil.getBackground());
143
144     for (IpnbOutputCell outputCell : myCell.getCellOutputs()) {
145       addOutputPanel(outputPanel, outputCell, true);
146     }
147
148     return outputPanel;
149   }
150
151
152
153   public void hideOutputPanel() {
154     myHideableOutputPanel.hideOutputPanel();
155   }
156
157   @NotNull
158   private MouseAdapter createShowOutputListener(final OnePixelSplitter splitter, final JPanel secondPanel, JLabel label) {
159     return new MouseAdapter() {
160       @Override
161       public void mouseClicked(MouseEvent e) {
162         if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 1) {
163           showOutputPanel();
164         }
165       }
166
167       @Override
168       public void mouseEntered(MouseEvent e) {
169         updateBackground(UIUtil.getListSelectionBackground());
170       }
171
172       @Override
173       public void mouseExited(MouseEvent e) {
174         updateBackground(IpnbEditorUtil.getBackground());
175       }
176
177       private void updateBackground(Color background) {
178         secondPanel.setBackground(background);
179         label.setBackground(background);
180       }
181
182       private void showOutputPanel() {
183         setOutputStateInCell(false);
184         updateBackground(IpnbEditorUtil.getBackground());
185         splitter.setFirstComponent(null);
186         final JPanel outputPanel = createOutputPanel();
187         splitter.setSecondComponent(outputPanel);
188       }
189     };
190   }
191
192   private void setOutputStateInCell(boolean isCollapsed) {
193     final Map<String, Object> metadata = myCell.getMetadata();
194     metadata.put("collapsed", isCollapsed);
195   }
196
197   private JPanel createToggleBar(OnePixelSplitter splitter) {
198     final JPanel panel = new JPanel(new BorderLayout());
199     final JLabel label = new JLabel(AllIcons.Actions.Down);
200     panel.setBackground(IpnbEditorUtil.getBackground());
201     label.setBackground(IpnbEditorUtil.getBackground());
202     panel.add(label, BorderLayout.CENTER);
203
204     panel.addMouseListener(createShowOutputListener(splitter, panel, label));
205
206     return panel;
207   }
208
209   private void addOutputPanel(@NotNull final JComponent mainPanel,
210                               @NotNull final IpnbOutputCell outputCell, boolean addPrompt) {
211     final IpnbEditorUtil.PromptType promptType = addPrompt ? IpnbEditorUtil.PromptType.Out : IpnbEditorUtil.PromptType.None;
212     final JPanel panel = new JPanel(new GridBagLayout());
213     panel.setBackground(IpnbEditorUtil.getBackground());
214     if (outputCell instanceof IpnbImageOutputCell) {
215       addPromptPanel(panel, myCell.getPromptNumber(), promptType,
216                      new IpnbImagePanel((IpnbImageOutputCell)outputCell, this));
217     }
218     else if (outputCell instanceof IpnbHtmlOutputCell) {
219       addPromptPanel(panel, myCell.getPromptNumber(), promptType,
220                      new IpnbHtmlPanel((IpnbHtmlOutputCell)outputCell, myParent.getIpnbFilePanel(), this));
221     }
222     else if (outputCell instanceof IpnbLatexOutputCell) {
223       addPromptPanel(panel, myCell.getPromptNumber(), promptType,
224                      new IpnbLatexPanel((IpnbLatexOutputCell)outputCell, myParent.getIpnbFilePanel(), this));
225     }
226     else if (outputCell instanceof IpnbErrorOutputCell) {
227       addPromptPanel(panel, myCell.getPromptNumber(), promptType,
228                      new IpnbErrorPanel((IpnbErrorOutputCell)outputCell, this));
229     }
230     else if (outputCell instanceof IpnbStreamOutputCell) {
231       addPromptPanel(panel, myCell.getPromptNumber(), IpnbEditorUtil.PromptType.None,
232                      new IpnbStreamPanel((IpnbStreamOutputCell)outputCell, this));
233     }
234     else if (outputCell.getSourceAsString() != null) {
235       addPromptPanel(panel, myCell.getPromptNumber(), promptType,
236                      new IpnbCodeOutputPanel<>(outputCell, myParent.getIpnbFilePanel(), this));
237     }
238     mainPanel.add(panel);
239   }
240
241   @Override
242   public void switchToEditing() {
243     setEditing(true);
244     final Container parent = getParent();
245     if (parent != null) {
246       parent.repaint();
247     }
248     UIUtil.requestFocus(myCodeSourcePanel.getEditor().getContentComponent());
249   }
250
251   @Override
252   public void runCell(boolean selectNext) {
253     mySelectNext = selectNext;
254     updateCellSource();
255     updatePrompt();
256     final IpnbConnectionManager connectionManager = IpnbConnectionManager.getInstance(myProject);
257     connectionManager.executeCell(this);
258     setEditing(false);
259   }
260
261   @Override
262   public boolean isModified() {
263     return true;
264   }
265
266   @Override
267   public void updateCellSource() {
268     final Document document = myCodeSourcePanel.getEditor().getDocument();
269     final String text = document.getText();
270     myCell.setSource(Arrays.asList(StringUtil.splitByLinesKeepSeparators(text)));
271   }
272
273   public void updatePrompt() {
274     final Application application = ApplicationManager.getApplication();
275     application.invokeAndWait(() -> {
276       myCell.setPromptNumber(-1);
277       myCell.removeCellOutputs();
278       myViewPanel.removeAll();
279
280       final JComponent panel = createViewPanel();
281       myViewPanel.add(panel);
282     }, ModalityState.stateForComponent(this));
283   }
284
285   public void finishExecution() {
286     final Application application = ApplicationManager.getApplication();
287     application.invokeAndWait(() -> {
288       final String promptText = IpnbEditorUtil.prompt(myCell.getPromptNumber(), IpnbEditorUtil.PromptType.In);
289       myPromptLabel.setText(promptText);
290       final IpnbFilePanel filePanel = myParent.getIpnbFilePanel();
291       setEditing(false);
292       filePanel.revalidateAndRepaint();
293       if (mySelectNext) {
294         filePanel.selectNext(this, true);
295       }
296     }, ModalityState.stateForComponent(this));
297   }
298
299   public void updatePanel(@Nullable final String replacementContent, @Nullable final IpnbOutputCell outputContent) {
300     final Application application = ApplicationManager.getApplication();
301     application.invokeAndWait(() -> {
302       if (replacementContent != null) {
303         myCell.setSource(Arrays.asList(StringUtil.splitByLinesKeepSeparators(replacementContent)));
304         String prompt = IpnbEditorUtil.prompt(null, IpnbEditorUtil.PromptType.In);
305         myCell.setPromptNumber(null);
306         myPromptLabel.setText(prompt);
307         application.runWriteAction(() -> myCodeSourcePanel.getEditor().getDocument().setText(replacementContent));
308       }
309       if (outputContent != null) {
310         myCell.addCellOutput(outputContent);
311         final JComponent component = myHideableOutputPanel.getSecondComponent();
312         if (component != null) {
313           addOutputPanel(component, outputContent, outputContent instanceof IpnbOutOutputCell);
314         }
315       }
316       final IpnbFilePanel filePanel = myParent.getIpnbFilePanel();
317       filePanel.revalidateAndRepaint();
318     }, ModalityState.stateForComponent(this));
319   }
320
321   @Override
322   public void updateCellView() {
323     myViewPanel.removeAll();
324     final JComponent panel = createViewPanel();
325     myViewPanel.add(panel);
326
327     final IpnbFilePanel filePanel = myParent.getIpnbFilePanel();
328     filePanel.revalidate();
329     filePanel.repaint();
330   }
331
332   @Override
333   public int getCaretPosition() {
334     return myCodeSourcePanel.getEditor().getCaretModel().getOffset();
335   }
336
337   @Nullable
338   @Override
339   public String getText(int from, int to) {
340     return myCodeSourcePanel.getEditor().getDocument().getText(new TextRange(from, to));
341   }
342
343   @Override
344   public String getText(int from) {
345     return getText(from, myCodeSourcePanel.getEditor().getDocument().getTextLength());
346   }
347
348   @SuppressWarnings({"CloneDoesntCallSuperClone", "CloneDoesntDeclareCloneNotSupportedException"})
349   @Override
350   protected Object clone() {
351     return new IpnbCodePanel(myProject, myParent, (IpnbCodeCell)myCell.clone());
352   }
353 }