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