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