replaced <code></code> with more concise {@code}
[idea/community.git] / plugins / ui-designer / src / com / intellij / uiDesigner / designSurface / InplaceEditingLayer.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
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 com.intellij.uiDesigner.designSurface;
17
18 import com.intellij.openapi.application.ApplicationManager;
19 import com.intellij.openapi.application.ModalityState;
20 import com.intellij.openapi.command.CommandProcessor;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.wm.FocusWatcher;
23 import com.intellij.openapi.wm.ex.IdeFocusTraversalPolicy;
24 import com.intellij.openapi.wm.ex.LayoutFocusTraversalPolicyExt;
25 import com.intellij.uiDesigner.FormEditingUtil;
26 import com.intellij.uiDesigner.UIDesignerBundle;
27 import com.intellij.uiDesigner.componentTree.ComponentSelectionListener;
28 import com.intellij.uiDesigner.propertyInspector.InplaceContext;
29 import com.intellij.uiDesigner.propertyInspector.Property;
30 import com.intellij.uiDesigner.propertyInspector.PropertyEditor;
31 import com.intellij.uiDesigner.propertyInspector.PropertyEditorAdapter;
32 import com.intellij.uiDesigner.radComponents.RadComponent;
33 import org.jetbrains.annotations.NotNull;
34 import org.jetbrains.annotations.Nullable;
35
36 import javax.swing.*;
37 import java.awt.*;
38 import java.awt.event.FocusEvent;
39 import java.awt.event.MouseEvent;
40
41 /**
42  * @author Anton Katilin
43  * @author Vladimir Kondratyev
44  */
45 public final class InplaceEditingLayer extends JComponent{
46   private static final Logger LOG = Logger.getInstance("#com.intellij.uiDesigner.InplaceEditingLayer");
47
48   private final GuiEditor myEditor;
49   /**
50    * Trackes focus movements inside myInplaceEditorComponent
51    */
52   private final MyFocusWatcher myFocusWatcher;
53   /**
54    * Commits or cancels editing
55    */
56   private final MyPropertyEditorListener myPropertyEditorListener;
57   /**
58    * The component which is currently edited with inplace editor.
59    * This component can be null.
60    */
61   private RadComponent myInplaceComponent;
62   /**
63    * Currently edited inplace property
64    */
65   private Property myInplaceProperty;
66   /**
67    * Current inplace editor
68    */
69   private PropertyEditor myInplaceEditor;
70   /**
71    * JComponent which is used as inplace editor
72    */
73   private JComponent myInplaceEditorComponent;
74   /**
75    * Preferred bounds of the inplace editor component
76    */
77   private Rectangle myPreferredBounds;
78   /**
79    * If {@code true} then we do not have to react on own events
80    */
81   private boolean myInsideChange;
82
83   public InplaceEditingLayer(@NotNull final GuiEditor editor) {
84     myEditor = editor;
85     myEditor.addComponentSelectionListener(new MyComponentSelectionListener());
86     myFocusWatcher = new MyFocusWatcher();
87     myPropertyEditorListener = new MyPropertyEditorListener();
88   }
89
90   /**
91    * This is optimization. We do not need to invalidate Swing hierarchy
92    * upper than InplaceEditingLayer.
93    */
94   public boolean isValidateRoot() {
95     return true;
96   }
97
98   /**
99    * When there is an inplace editor we "listen" all mouse event
100    * and finish editing by any MOUSE_PRESSED or MOUSE_RELEASED event.
101    * We are acting like yet another glass pane over the standard glass layer.
102    */
103   protected void processMouseEvent(final MouseEvent e) {
104     if(
105       myInplaceComponent != null &&
106       (MouseEvent.MOUSE_PRESSED == e.getID() || MouseEvent.MOUSE_RELEASED == e.getID())
107     ){
108       finishInplaceEditing();
109     }
110     // [vova] this is very important! Without this code Swing doen't close popup menu on our
111     // layered pane. Swing adds MouseListeners to all component to close popup. If we do not
112     // invoke super then we lock all mouse listeners.
113     super.processMouseEvent(e);
114   }
115
116   /**
117    * @return whether the layer is in "editing" state or not
118    */
119   public boolean isEditing(){
120     return myInplaceComponent != null;
121   }
122
123   /**
124    * Starts editing of "inplace" property for the component at the
125    * specified point {@code (x, y)}.
126    *
127    * @param x x coordinate in the editor coordinate system
128    * @param y y coordinate in the editor coordinate system
129    */
130   public void startInplaceEditing(final int x, final int y){
131     final RadComponent inplaceComponent = FormEditingUtil.getRadComponentAt(myEditor.getRootContainer(), x, y);
132     if(inplaceComponent == null){ // nothing to edit
133       return;
134     }
135
136     // Try to find property with inplace editor
137     final Point p = SwingUtilities.convertPoint(this, x, y, inplaceComponent.getDelegee());
138     final Property inplaceProperty = inplaceComponent.getInplaceProperty(p.x, p.y);
139     if (inplaceProperty != null) {
140       final Rectangle bounds = inplaceComponent.getInplaceEditorBounds(inplaceProperty, p.x, p.y);
141       startInplaceEditing(inplaceComponent, inplaceProperty, bounds, new InplaceContext(true));
142     }
143   }
144
145   public void startInplaceEditing(@NotNull final RadComponent inplaceComponent,
146                                   @Nullable final Property property,
147                                   @Nullable final Rectangle bounds,
148                                   final InplaceContext context) {
149     myInplaceProperty = property;
150     if(myInplaceProperty == null){
151       return;
152     }
153
154     if (!myEditor.ensureEditable()) {
155       myInplaceProperty = null;
156       return;
157     }
158
159     // Now we have to cancel previous inplace editing (if any)
160
161     // Start new inplace editing
162     myInplaceComponent = inplaceComponent;
163     myInplaceEditor  = myInplaceProperty.getEditor();
164     LOG.assertTrue(myInplaceEditor != null);
165
166     // 1. Get editor component
167     myInplaceEditorComponent = myInplaceEditor.getComponent(
168       myInplaceComponent,
169       context.isKeepInitialValue() ? myInplaceProperty.getValue(myInplaceComponent) : null,
170       context
171     );
172
173     if (context.isModalDialogDisplayed()) {  // ListModel, for example
174       finishInplaceEditing();
175       return;
176     }
177
178     LOG.assertTrue(myInplaceEditorComponent != null);
179     myInplaceEditor.addPropertyEditorListener(myPropertyEditorListener);
180
181     // 2. Set editor component bounds
182     final Dimension prefSize = myInplaceEditorComponent.getPreferredSize();
183     if(bounds != null){ // use bounds provided by the component itself
184       final Point _p = SwingUtilities.convertPoint(myInplaceComponent.getDelegee(), bounds.x, bounds.y, this);
185       myPreferredBounds = new Rectangle(_p.x, _p.y, bounds.width, bounds.height);
186     }
187     else{ // set some default bounds
188       final Point _p = SwingUtilities.convertPoint(myInplaceComponent.getDelegee(), 0, 0, this);
189       myPreferredBounds = new Rectangle(_p.x,  _p.y, myInplaceComponent.getWidth(), myInplaceComponent.getHeight());
190     }
191     myInplaceEditorComponent.setBounds(
192       myPreferredBounds.x,
193       myPreferredBounds.y + (myPreferredBounds.height - prefSize.height)/2,
194       Math.min(Math.max(prefSize.width, myPreferredBounds.width), getWidth() - myPreferredBounds.x),
195       prefSize.height
196     );
197
198     // 3. Add it into layer
199     add(myInplaceEditorComponent);
200     myInplaceEditorComponent.revalidate();
201
202     // 4. Request focus into proper component
203     JComponent componentToFocus = myInplaceEditor.getPreferredFocusedComponent(myInplaceEditorComponent);
204     if (componentToFocus == null) {
205       componentToFocus = IdeFocusTraversalPolicy.getPreferredFocusedComponent(myInplaceEditorComponent);
206     }
207     if (componentToFocus == null) {
208       componentToFocus = myInplaceEditorComponent;
209     }
210     if (componentToFocus.requestFocusInWindow()) {
211       myFocusWatcher.install(myInplaceEditorComponent);
212     }
213     else {
214       grabFocus();
215       final JComponent finalComponentToFocus = componentToFocus;
216       ApplicationManager.getApplication().invokeLater(() -> {
217         finalComponentToFocus.requestFocusInWindow();
218         myFocusWatcher.install(myInplaceEditorComponent);
219       });
220     }
221
222     // 5. Block any mouse event to finish editing by any of them
223     enableEvents(MouseEvent.MOUSE_EVENT_MASK);
224
225     repaint();
226   }
227
228   private void adjustEditorComponentSize(){
229     if (myInplaceEditorComponent == null) return;
230     final Dimension preferredSize = myInplaceEditorComponent.getPreferredSize();
231     int width = Math.max(preferredSize.width, myPreferredBounds.width);
232     // Editor component should not be extended to invisible area
233     width = Math.min(width, getWidth() - myInplaceEditorComponent.getX());
234     myInplaceEditorComponent.setSize(width, myInplaceEditorComponent.getHeight());
235     myInplaceEditorComponent.revalidate();
236   }
237
238   /**
239    * Finishes current inplace editing
240    */
241   public void finishInplaceEditing(){
242     if (myInplaceComponent == null || myInsideChange) { // nothing to finish
243       return;
244     }
245     myInsideChange = true;
246     try{
247       // 1. Apply new value to the component
248       LOG.assertTrue(myInplaceEditor != null);
249       if (!myEditor.isUndoRedoInProgress()) {
250         CommandProcessor.getInstance().executeCommand(
251           myInplaceComponent.getProject(),
252           () -> {
253             try {
254               final Object value = myInplaceEditor.getValue();
255               myInplaceProperty.setValue(myInplaceComponent, value);
256             }
257             catch (Exception ignored) {
258             }
259             myEditor.refreshAndSave(true);
260           }, UIDesignerBundle.message("command.set.property.value"), null);
261       }
262       // 2. Remove editor from the layer
263
264       if (myInplaceEditorComponent != null) {  // reenterability guard
265         removeInplaceEditorComponent();
266         myFocusWatcher.deinstall(myInplaceEditorComponent);
267       }
268
269       myInplaceEditor.removePropertyEditorListener(myPropertyEditorListener);
270
271       myInplaceComponent = null;
272       myInplaceEditorComponent = null;
273       myInplaceComponent = null;
274
275       // 3. Let AWT work
276       disableEvents(MouseEvent.MOUSE_EVENT_MASK);
277     }finally{
278       myInsideChange = false;
279     }
280
281     repaint();
282   }
283
284   /**
285    * Cancells current inplace editing
286    */
287   private void cancelInplaceEditing(){
288     if(myInplaceComponent == null || myInsideChange){ // nothing to finish
289       return;
290     }
291     myInsideChange = true;
292     try{
293       // 1. Remove editor from the layer
294       LOG.assertTrue(myInplaceProperty != null);
295       LOG.assertTrue(myInplaceEditor != null);
296
297       removeInplaceEditorComponent();
298
299       myInplaceEditor.removePropertyEditorListener(myPropertyEditorListener);
300       myFocusWatcher.deinstall(myInplaceEditorComponent);
301
302       myInplaceComponent = null;
303       myInplaceEditorComponent = null;
304       myInplaceComponent = null;
305
306       // 2. Let AWT work
307       disableEvents(MouseEvent.MOUSE_EVENT_MASK);
308     }finally{
309       myInsideChange = false;
310     }
311
312     repaint();
313   }
314
315   private void removeInplaceEditorComponent() {
316     // [vova] before removing component from Swing tree we have to
317     // request component into glass layer. Otherwise focus from component being removed
318     // can go to some RadComponent.
319
320     LayoutFocusTraversalPolicyExt.setOverridenDefaultComponent(myEditor.getGlassLayer());
321     try {
322       remove(myInplaceEditorComponent);
323     }
324     finally {
325       LayoutFocusTraversalPolicyExt.setOverridenDefaultComponent(null);
326     }
327   }
328
329   /**
330    * Finish inplace editing when selection changes
331    */
332   private final class MyComponentSelectionListener implements ComponentSelectionListener{
333     public void selectedComponentChanged(final GuiEditor source) {
334       finishInplaceEditing();
335     }
336   }
337
338   /**
339    * Finish inplace editing when inplace editor component loses focus
340    */
341   private final class MyFocusWatcher extends FocusWatcher{
342     protected void focusLostImpl(final FocusEvent e) {
343       final Component opposite = e.getOppositeComponent();
344       if(
345          e.isTemporary() ||
346          opposite != null && SwingUtilities.isDescendingFrom(opposite, getTopComponent())
347       ){
348         // Do nothing if focus moves inside top component hierarchy
349         return;
350       }
351       // [vova] we need LaterInvocator here to prevent write-access assertions
352       ApplicationManager.getApplication().invokeLater(() -> finishInplaceEditing(), ModalityState.NON_MODAL);
353     }
354   }
355
356   /**
357    * Finishes editing by "Enter" and cancels editing by "Esc"
358    */
359   private final class MyPropertyEditorListener extends PropertyEditorAdapter{
360     public void valueCommitted(final PropertyEditor source, final boolean continueEditing, final boolean closeEditorOnError) {
361       finishInplaceEditing();
362     }
363
364     public void editingCanceled(final PropertyEditor source) {
365       cancelInplaceEditing();
366     }
367
368     public void preferredSizeChanged(final PropertyEditor source) {
369       adjustEditorComponentSize();
370     }
371   }
372 }