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