PY-19960 PY-14096 Merge right-click menu creation for toggle output and split/merge...
[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
131     final JPanel panel = new JPanel(new GridBagLayout());
132     panel.setBackground(IpnbEditorUtil.getBackground());
133     addPromptPanel(panel, myCell.getPromptNumber(), IpnbEditorUtil.PromptType.In, myCodeSourcePanel);
134
135     final JPanel topComponent = new JPanel(new BorderLayout());
136     topComponent.add(panel, BorderLayout.PAGE_START);
137     return topComponent;
138   }
139
140   @NotNull
141   private JPanel createOutputPanel() {
142     final JPanel outputPanel = new JPanel(new VerticalFlowLayout(VerticalFlowLayout.TOP, true, false));
143     outputPanel.setBackground(IpnbEditorUtil.getBackground());
144
145     for (IpnbOutputCell outputCell : myCell.getCellOutputs()) {
146       addOutputPanel(outputPanel, outputCell, this, true);
147     }
148
149     return outputPanel;
150   }
151
152   
153
154   public void hideOutputPanel() {
155     myHideableOutputPanel.hideOutputPanel();
156   }
157
158   @NotNull
159   private MouseAdapter createShowOutputListener(final OnePixelSplitter splitter, final JPanel secondPanel, JLabel label) {
160     return new MouseAdapter() {
161       @Override
162       public void mouseClicked(MouseEvent e) {
163         if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 1) {
164           showOutputPanel();
165         }
166       }
167
168       @Override
169       public void mouseEntered(MouseEvent e) {
170         updateBackground(UIUtil.getListSelectionBackground());
171       }
172
173       @Override
174       public void mouseExited(MouseEvent e) {
175         updateBackground(IpnbEditorUtil.getBackground());
176       }
177
178       private void updateBackground(Color background) {
179         secondPanel.setBackground(background);
180         label.setBackground(background);
181       }
182
183       private void showOutputPanel() {
184         setOutputStateInCell(false);
185         updateBackground(IpnbEditorUtil.getBackground());
186         splitter.setFirstComponent(null);
187         final JPanel outputPanel = createOutputPanel();
188         splitter.setSecondComponent(outputPanel);
189       }
190     };
191   }
192
193   private void setOutputStateInCell(boolean isCollapsed) {
194     final Map<String, Object> metadata = myCell.getMetadata();
195     metadata.put("collapsed", isCollapsed);
196   }
197
198   private JPanel createToggleBar(OnePixelSplitter splitter) {
199     final JPanel panel = new JPanel(new BorderLayout());
200     final JLabel label = new JLabel(AllIcons.Actions.Down);
201     panel.setBackground(IpnbEditorUtil.getBackground());
202     label.setBackground(IpnbEditorUtil.getBackground());
203     panel.add(label, BorderLayout.CENTER);
204
205     panel.addMouseListener(createShowOutputListener(splitter, panel, label));
206
207     return panel;
208   }
209
210   private void addOutputPanel(@NotNull final JComponent mainPanel,
211                               @NotNull final IpnbOutputCell outputCell, IpnbCodePanel ipnbCodePanel, boolean addPrompt) {
212     final IpnbEditorUtil.PromptType promptType = addPrompt ? IpnbEditorUtil.PromptType.Out : IpnbEditorUtil.PromptType.None;
213     final JPanel panel = new JPanel(new GridBagLayout());
214     panel.setBackground(IpnbEditorUtil.getBackground());
215     if (outputCell instanceof IpnbImageOutputCell) {
216       addPromptPanel(panel, myCell.getPromptNumber(), promptType,
217                      new IpnbImagePanel((IpnbImageOutputCell)outputCell, ipnbCodePanel));
218     }
219     else if (outputCell instanceof IpnbHtmlOutputCell) {
220       addPromptPanel(panel, myCell.getPromptNumber(), promptType,
221                      new IpnbHtmlPanel((IpnbHtmlOutputCell)outputCell, myParent.getIpnbFilePanel(), ipnbCodePanel));
222     }
223     else if (outputCell instanceof IpnbLatexOutputCell) {
224       addPromptPanel(panel, myCell.getPromptNumber(), promptType,
225                      new IpnbLatexPanel((IpnbLatexOutputCell)outputCell, myParent.getIpnbFilePanel(), ipnbCodePanel));
226     }
227     else if (outputCell instanceof IpnbErrorOutputCell) {
228       addPromptPanel(panel, myCell.getPromptNumber(), promptType,
229                      new IpnbErrorPanel((IpnbErrorOutputCell)outputCell, ipnbCodePanel));
230     }
231     else if (outputCell instanceof IpnbStreamOutputCell) {
232       addPromptPanel(panel, myCell.getPromptNumber(), IpnbEditorUtil.PromptType.None,
233                      new IpnbStreamPanel((IpnbStreamOutputCell)outputCell, ipnbCodePanel));
234     }
235     else if (outputCell.getSourceAsString() != null) {
236       addPromptPanel(panel, myCell.getPromptNumber(), promptType,
237                      new IpnbCodeOutputPanel<>(outputCell, myParent.getIpnbFilePanel(), ipnbCodePanel));
238     }
239     mainPanel.add(panel);
240   }
241
242   @Override
243   public void switchToEditing() {
244     setEditing(true);
245     final Container parent = getParent();
246     if (parent != null) {
247       parent.repaint();
248     }
249     UIUtil.requestFocus(myCodeSourcePanel.getEditor().getContentComponent());
250   }
251
252   @Override
253   public void runCell(boolean selectNext) {
254     mySelectNext = selectNext;
255     updateCellSource();
256     isRunning = true;
257     updatePanel(null, null);
258     final IpnbConnectionManager connectionManager = IpnbConnectionManager.getInstance(myProject);
259     connectionManager.executeCell(this);
260     setEditing(false);
261   }
262
263   @Override
264   public boolean isModified() {
265     return true;
266   }
267
268   @Override
269   public void updateCellSource() {
270     final Document document = myCodeSourcePanel.getEditor().getDocument();
271     final String text = document.getText();
272     myCell.setSource(Arrays.asList(StringUtil.splitByLinesKeepSeparators(text)));
273   }
274
275   public void updatePanel(@Nullable final String replacementContent, @Nullable final List<IpnbOutputCell> outputContent) {
276     final Application application = ApplicationManager.getApplication();
277     application.invokeAndWait(() -> {
278       if (replacementContent != null) {
279         myCell.setSource(Arrays.asList(StringUtil.splitByLinesKeepSeparators(replacementContent)));
280         application.runWriteAction(() -> myCodeSourcePanel.getEditor().getDocument().setText(replacementContent));
281       }
282       myCell.removeCellOutputs();
283       myViewPanel.removeAll();
284
285       isRunning = false;
286       if (outputContent != null) {
287         for (IpnbOutputCell output : outputContent) {
288           myCell.addCellOutput(output);
289         }
290       }
291
292       final JComponent panel = createViewPanel();
293       myViewPanel.add(panel);
294
295       final IpnbFilePanel filePanel = myParent.getIpnbFilePanel();
296       setEditing(false);
297       filePanel.revalidateAndRepaint();
298       if (mySelectNext && (replacementContent != null || outputContent != null)) {
299         filePanel.selectNext(this, true);
300       }
301     }, ModalityState.stateForComponent(this));
302   }
303
304   @Override
305   public void updateCellView() {
306     myViewPanel.removeAll();
307     final JComponent panel = createViewPanel();
308     myViewPanel.add(panel);
309
310     final IpnbFilePanel filePanel = myParent.getIpnbFilePanel();
311     filePanel.revalidate();
312     filePanel.repaint();
313   }
314
315   @Override
316   public int getCaretPosition() {
317     return myCodeSourcePanel.getEditor().getCaretModel().getOffset();
318   }
319
320   @Nullable
321   @Override
322   public String getText(int from, int to) {
323     return myCodeSourcePanel.getEditor().getDocument().getText(new TextRange(from, to));
324   }
325
326   @Override
327   public String getText(int from) {
328     return getText(from, myCodeSourcePanel.getEditor().getDocument().getTextLength());
329   }
330
331   @SuppressWarnings({"CloneDoesntCallSuperClone", "CloneDoesntDeclareCloneNotSupportedException"})
332   @Override
333   protected Object clone() {
334     return new IpnbCodePanel(myProject, myParent, (IpnbCodeCell)myCell.clone());
335   }
336 }