Revert "Debugger: Image/Icon viewer based on standard image viewer"
[idea/community.git] / images / src / org / intellij / images / editor / impl / ImageEditorUI.java
1 /*
2  * Copyright 2004-2005 Alexey Efimov
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 org.intellij.images.editor.impl;
17
18 import com.intellij.ide.CopyPasteSupport;
19 import com.intellij.ide.CopyProvider;
20 import com.intellij.ide.DeleteProvider;
21 import com.intellij.ide.PsiActionSupportFactory;
22 import com.intellij.openapi.actionSystem.*;
23 import com.intellij.openapi.ide.CopyPasteManager;
24 import com.intellij.openapi.ui.Messages;
25 import com.intellij.openapi.util.text.StringUtil;
26 import com.intellij.openapi.vfs.VirtualFile;
27 import com.intellij.psi.PsiElement;
28 import com.intellij.psi.PsiManager;
29 import com.intellij.ui.IdeBorderFactory;
30 import com.intellij.ui.PopupHandler;
31 import com.intellij.ui.ScrollPaneFactory;
32 import com.intellij.ui.components.JBLayeredPane;
33 import com.intellij.ui.components.Magnificator;
34 import com.intellij.util.ui.UIUtil;
35 import org.intellij.images.ImagesBundle;
36 import org.intellij.images.editor.ImageDocument;
37 import org.intellij.images.editor.ImageEditor;
38 import org.intellij.images.editor.ImageZoomModel;
39 import org.intellij.images.editor.actionSystem.ImageEditorActions;
40 import org.intellij.images.options.*;
41 import org.intellij.images.ui.ImageComponent;
42 import org.intellij.images.ui.ImageComponentDecorator;
43 import org.jetbrains.annotations.NonNls;
44 import org.jetbrains.annotations.NotNull;
45 import org.jetbrains.annotations.Nullable;
46
47 import javax.swing.*;
48 import javax.swing.event.ChangeEvent;
49 import javax.swing.event.ChangeListener;
50 import java.awt.*;
51 import java.awt.datatransfer.DataFlavor;
52 import java.awt.datatransfer.Transferable;
53 import java.awt.datatransfer.UnsupportedFlavorException;
54 import java.awt.event.MouseAdapter;
55 import java.awt.event.MouseEvent;
56 import java.awt.event.MouseWheelEvent;
57 import java.awt.event.MouseWheelListener;
58 import java.awt.image.BufferedImage;
59 import java.awt.image.ColorModel;
60 import java.io.IOException;
61 import java.util.Locale;
62
63 /**
64  * Image editor UI
65  *
66  * @author <a href="mailto:aefimov.box@gmail.com">Alexey Efimov</a>
67  */
68 final class ImageEditorUI extends JPanel implements DataProvider, CopyProvider {
69   @NonNls
70   private static final String IMAGE_PANEL = "image";
71   @NonNls
72   private static final String ERROR_PANEL = "error";
73
74   private final ImageEditor editor;
75   private final DeleteProvider deleteProvider;
76   private final CopyPasteSupport copyPasteSupport;
77
78   private final ImageZoomModel zoomModel = new ImageZoomModelImpl();
79   private final ImageWheelAdapter wheelAdapter = new ImageWheelAdapter();
80   private final ChangeListener changeListener = new DocumentChangeListener();
81   private final ImageComponent imageComponent = new ImageComponent();
82   private final JPanel contentPanel;
83   private final JLabel infoLabel;
84
85   ImageEditorUI(ImageEditor editor, EditorOptions editorOptions) {
86     this.editor = editor;
87     final PsiActionSupportFactory factory = PsiActionSupportFactory.getInstance();
88     if (factory != null) {
89       copyPasteSupport =
90         factory.createPsiBasedCopyPasteSupport(editor.getProject(), this, new PsiActionSupportFactory.PsiElementSelector() {
91           public PsiElement[] getSelectedElements() {
92             PsiElement[] data = LangDataKeys.PSI_ELEMENT_ARRAY.getData(ImageEditorUI.this);
93             return data == null ? PsiElement.EMPTY_ARRAY : data;
94           }
95         });
96     } else {
97       copyPasteSupport = null;
98     }
99
100     deleteProvider = factory == null ? null : factory.createPsiBasedDeleteProvider();
101
102     ImageDocument document = imageComponent.getDocument();
103     document.addChangeListener(changeListener);
104
105     // Set options
106     TransparencyChessboardOptions chessboardOptions = editorOptions.getTransparencyChessboardOptions();
107     GridOptions gridOptions = editorOptions.getGridOptions();
108     imageComponent.setTransparencyChessboardCellSize(chessboardOptions.getCellSize());
109     imageComponent.setTransparencyChessboardWhiteColor(chessboardOptions.getWhiteColor());
110     imageComponent.setTransparencyChessboardBlankColor(chessboardOptions.getBlackColor());
111     imageComponent.setGridLineZoomFactor(gridOptions.getLineZoomFactor());
112     imageComponent.setGridLineSpan(gridOptions.getLineSpan());
113     imageComponent.setGridLineColor(gridOptions.getLineColor());
114
115     // Create layout
116     ImageContainerPane view = new ImageContainerPane(imageComponent);
117     view.addMouseListener(new EditorMouseAdapter());
118     view.addMouseListener(new FocusRequester());
119
120     JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(view);
121     scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
122     scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
123
124     // Zoom by wheel listener
125     scrollPane.addMouseWheelListener(wheelAdapter);
126
127     // Construct UI
128     setLayout(new BorderLayout());
129
130     ActionManager actionManager = ActionManager.getInstance();
131     ActionGroup actionGroup = (ActionGroup)actionManager.getAction(ImageEditorActions.GROUP_TOOLBAR);
132     ActionToolbar actionToolbar = actionManager.createActionToolbar(
133       ImageEditorActions.ACTION_PLACE, actionGroup, true
134     );
135     actionToolbar.setTargetComponent(this);
136
137     JComponent toolbarPanel = actionToolbar.getComponent();
138     toolbarPanel.addMouseListener(new FocusRequester());
139
140     JLabel errorLabel = new JLabel(
141       ImagesBundle.message("error.broken.image.file.format"),
142       Messages.getErrorIcon(), SwingConstants.CENTER
143     );
144
145     JPanel errorPanel = new JPanel(new BorderLayout());
146     errorPanel.add(errorLabel, BorderLayout.CENTER);
147
148     contentPanel = new JPanel(new CardLayout());
149     contentPanel.add(scrollPane, IMAGE_PANEL);
150     contentPanel.add(errorPanel, ERROR_PANEL);
151
152     JPanel topPanel = new JPanel(new BorderLayout());
153     topPanel.add(toolbarPanel, BorderLayout.WEST);
154     infoLabel = new JLabel((String)null, SwingConstants.RIGHT);
155     infoLabel.setBorder(IdeBorderFactory.createEmptyBorder(0, 0, 0, 2));
156     topPanel.add(infoLabel, BorderLayout.EAST);
157
158     add(topPanel, BorderLayout.NORTH);
159     add(contentPanel, BorderLayout.CENTER);
160
161     updateInfo();
162   }
163
164   private void updateInfo() {
165     ImageDocument document = imageComponent.getDocument();
166     BufferedImage image = document.getValue();
167     if (image != null) {
168       ColorModel colorModel = image.getColorModel();
169       String format = document.getFormat();
170       if (format == null) {
171         format = ImagesBundle.message("unknown.format");
172       } else {
173         format = format.toUpperCase(Locale.ENGLISH);
174       }
175       VirtualFile file = editor.getFile();
176       infoLabel.setText(
177         ImagesBundle.message("image.info",
178                              image.getWidth(), image.getHeight(), format,
179                              colorModel.getPixelSize(), file != null ? StringUtil.formatFileSize(file.getLength()) : ""));
180     } else {
181       infoLabel.setText(null);
182     }
183   }
184
185   @SuppressWarnings("UnusedDeclaration")
186   JComponent getContentComponent() {
187     return contentPanel;
188   }
189
190   ImageComponent getImageComponent() {
191     return imageComponent;
192   }
193
194   void dispose() {
195     imageComponent.removeMouseWheelListener(wheelAdapter);
196     imageComponent.getDocument().removeChangeListener(changeListener);
197
198     removeAll();
199   }
200
201   ImageZoomModel getZoomModel() {
202     return zoomModel;
203   }
204
205   private final class ImageContainerPane extends JBLayeredPane {
206     private final ImageComponent imageComponent;
207
208     public ImageContainerPane(final ImageComponent imageComponent) {
209       this.imageComponent = imageComponent;
210       add(imageComponent);
211
212       putClientProperty(Magnificator.CLIENT_PROPERTY_KEY, new Magnificator() {
213         @Override
214         public Point magnify(double scale, Point at) {
215           Point locationBefore = imageComponent.getLocation();
216           ImageZoomModel model = editor.getZoomModel();
217           double factor = model.getZoomFactor();
218           model.setZoomFactor(scale * factor);
219           return new Point(((int)((at.x - Math.max(scale > 1.0 ? locationBefore.x : 0, 0)) * scale)), 
220                            ((int)((at.y - Math.max(scale > 1.0 ? locationBefore.y : 0, 0)) * scale)));
221         }
222       });
223     }
224
225     private void centerComponents() {
226       Rectangle bounds = getBounds();
227       Point point = imageComponent.getLocation();
228       point.x = (bounds.width - imageComponent.getWidth()) / 2;
229       point.y = (bounds.height - imageComponent.getHeight()) / 2;
230       imageComponent.setLocation(point);
231     }
232
233     public void invalidate() {
234       centerComponents();
235       super.invalidate();
236     }
237
238     public Dimension getPreferredSize() {
239       return imageComponent.getSize();
240     }
241
242     @Override
243     protected void paintComponent(@NotNull Graphics g) {
244       super.paintComponent(g);
245       if (UIUtil.isUnderDarcula()) {
246         g.setColor(UIUtil.getControlColor().brighter());
247         g.fillRect(0, 0, getWidth(), getHeight());
248       }
249     }
250   }
251
252   private final class ImageWheelAdapter implements MouseWheelListener {
253     public void mouseWheelMoved(MouseWheelEvent e) {
254       Options options = OptionsManager.getInstance().getOptions();
255       EditorOptions editorOptions = options.getEditorOptions();
256       ZoomOptions zoomOptions = editorOptions.getZoomOptions();
257       if (zoomOptions.isWheelZooming() && e.isControlDown()) {
258         if (e.getWheelRotation() < 0) {
259           zoomModel.zoomOut();
260         } else {
261           zoomModel.zoomIn();
262         }
263         e.consume();
264       }
265     }
266   }
267
268   private class ImageZoomModelImpl implements ImageZoomModel {
269     private boolean myZoomLevelChanged = false;
270
271     public double getZoomFactor() {
272       Dimension size = imageComponent.getCanvasSize();
273       BufferedImage image = imageComponent.getDocument().getValue();
274       return image != null ? size.getWidth() / (double)image.getWidth() : 0.0d;
275     }
276
277     public void setZoomFactor(double zoomFactor) {
278       // Change current size
279       Dimension size = imageComponent.getCanvasSize();
280       BufferedImage image = imageComponent.getDocument().getValue();
281       if (image != null) {
282         size.setSize((double)image.getWidth() * zoomFactor, (double)image.getHeight() * zoomFactor);
283         imageComponent.setCanvasSize(size);
284       }
285
286       revalidate();
287       repaint();
288       myZoomLevelChanged = false;
289     }
290
291     private double getMinimumZoomFactor() {
292       BufferedImage image = imageComponent.getDocument().getValue();
293       return image != null ? 1.0d / image.getWidth() : 0.0d;
294     }
295
296     public void zoomOut() {
297       double factor = getZoomFactor();
298       if (factor > 1.0d) {
299         // Macro
300         setZoomFactor(factor / 2.0d);
301       } else {
302         // Micro
303         double minFactor = getMinimumZoomFactor();
304         double stepSize = (1.0d - minFactor) / MICRO_ZOOM_LIMIT;
305         int step = (int)Math.ceil((1.0d - factor) / stepSize);
306
307         setZoomFactor(1.0d - stepSize * (step + 1));
308       }
309       myZoomLevelChanged = true;
310     }
311
312     public void zoomIn() {
313       double factor = getZoomFactor();
314       if (factor >= 1.0d) {
315         // Macro
316         setZoomFactor(factor * 2.0d);
317       } else {
318         // Micro
319         double minFactor = getMinimumZoomFactor();
320         double stepSize = (1.0d - minFactor) / MICRO_ZOOM_LIMIT;
321         double step = (1.0d - factor) / stepSize;
322
323         setZoomFactor(1.0d - stepSize * (step - 1));
324       }
325       myZoomLevelChanged = true;
326     }
327
328     public boolean canZoomOut() {
329       double factor = getZoomFactor();
330       double minFactor = getMinimumZoomFactor();
331       double stepSize = (1.0 - minFactor) / MICRO_ZOOM_LIMIT;
332       double step = Math.ceil((1.0 - factor) / stepSize);
333
334       return step < MICRO_ZOOM_LIMIT;
335     }
336
337     public boolean canZoomIn() {
338       double zoomFactor = getZoomFactor();
339       return zoomFactor < MACRO_ZOOM_LIMIT;
340     }
341
342     public boolean isZoomLevelChanged() {
343       return myZoomLevelChanged;
344     }
345   }
346
347   private class DocumentChangeListener implements ChangeListener {
348     public void stateChanged(@NotNull ChangeEvent e) {
349       ImageDocument document = imageComponent.getDocument();
350       BufferedImage value = document.getValue();
351
352       CardLayout layout = (CardLayout)contentPanel.getLayout();
353       layout.show(contentPanel, value != null ? IMAGE_PANEL : ERROR_PANEL);
354
355       updateInfo();
356
357       revalidate();
358       repaint();
359     }
360   }
361
362   private class FocusRequester extends MouseAdapter {
363     public void mousePressed(@NotNull MouseEvent e) {
364       requestFocus();
365     }
366   }
367
368   private static final class EditorMouseAdapter extends PopupHandler {
369     @Override
370     public void invokePopup(Component comp, int x, int y) {
371       // Single right click
372       ActionManager actionManager = ActionManager.getInstance();
373       ActionGroup actionGroup = (ActionGroup)actionManager.getAction(ImageEditorActions.GROUP_POPUP);
374       ActionPopupMenu menu = actionManager.createActionPopupMenu(ImageEditorActions.ACTION_PLACE, actionGroup);
375       JPopupMenu popupMenu = menu.getComponent();
376       popupMenu.pack();
377       popupMenu.show(comp, x, y);
378     }
379   }
380
381
382   @Nullable
383   public Object getData(String dataId) {
384
385     if (CommonDataKeys.PROJECT.is(dataId)) {
386       return editor.getProject();
387     } else if (CommonDataKeys.VIRTUAL_FILE.is(dataId)) {
388       return editor.getFile();
389     } else if (CommonDataKeys.VIRTUAL_FILE_ARRAY.is(dataId)) {
390       return new VirtualFile[]{editor.getFile()};
391     } else if (CommonDataKeys.PSI_FILE.is(dataId)) {
392       return getData(CommonDataKeys.PSI_ELEMENT.getName());
393     } else if (CommonDataKeys.PSI_ELEMENT.is(dataId)) {
394       VirtualFile file = editor.getFile();
395       return file != null && file.isValid() ? PsiManager.getInstance(editor.getProject()).findFile(file) : null;
396     } else if (LangDataKeys.PSI_ELEMENT_ARRAY.is(dataId)) {
397       return new PsiElement[]{(PsiElement)getData(CommonDataKeys.PSI_ELEMENT.getName())};
398     } else if (PlatformDataKeys.COPY_PROVIDER.is(dataId) && copyPasteSupport != null) {
399       return this;
400     } else if (PlatformDataKeys.CUT_PROVIDER.is(dataId) && copyPasteSupport != null) {
401       return copyPasteSupport.getCutProvider();
402     } else if (PlatformDataKeys.DELETE_ELEMENT_PROVIDER.is(dataId)) {
403       return deleteProvider;
404     } else if (ImageComponentDecorator.DATA_KEY.is(dataId)) {
405       return editor;
406     }
407
408     return null;
409   }
410
411   @Override
412   public void performCopy(@NotNull DataContext dataContext) {
413     ImageDocument document = imageComponent.getDocument();
414     BufferedImage image = document.getValue();
415     CopyPasteManager.getInstance().setContents(new ImageTransferable(image));
416   }
417
418   @Override
419   public boolean isCopyEnabled(@NotNull DataContext dataContext) {
420     return true;
421   }
422
423   @Override
424   public boolean isCopyVisible(@NotNull DataContext dataContext) {
425     return true;
426   }
427
428   private static class ImageTransferable implements Transferable {
429     private final BufferedImage myImage;
430
431     public ImageTransferable(@NotNull BufferedImage image) {
432       myImage = image;
433     }
434
435     @Override
436     public DataFlavor[] getTransferDataFlavors() {
437       return new DataFlavor[] { DataFlavor.imageFlavor };
438     }
439
440     @Override
441     public boolean isDataFlavorSupported(DataFlavor dataFlavor) {
442       return DataFlavor.imageFlavor.equals(dataFlavor);
443     }
444
445     @Override
446     public Object getTransferData(DataFlavor dataFlavor) throws UnsupportedFlavorException, IOException {
447       if (!DataFlavor.imageFlavor.equals(dataFlavor)) {
448         throw new UnsupportedFlavorException(dataFlavor);
449       }
450       return myImage;
451     }
452   }
453 }