| /* |
| * Copyright 2000-2009 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.intellij.uiDesigner.designSurface; |
| |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.application.ModalityState; |
| import com.intellij.openapi.command.CommandProcessor; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.wm.FocusWatcher; |
| import com.intellij.openapi.wm.ex.IdeFocusTraversalPolicy; |
| import com.intellij.openapi.wm.ex.LayoutFocusTraversalPolicyExt; |
| import com.intellij.uiDesigner.FormEditingUtil; |
| import com.intellij.uiDesigner.UIDesignerBundle; |
| import com.intellij.uiDesigner.componentTree.ComponentSelectionListener; |
| import com.intellij.uiDesigner.propertyInspector.Property; |
| import com.intellij.uiDesigner.propertyInspector.PropertyEditor; |
| import com.intellij.uiDesigner.propertyInspector.PropertyEditorAdapter; |
| import com.intellij.uiDesigner.propertyInspector.InplaceContext; |
| import com.intellij.uiDesigner.radComponents.RadComponent; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import java.awt.*; |
| import java.awt.event.FocusEvent; |
| import java.awt.event.MouseEvent; |
| |
| /** |
| * @author Anton Katilin |
| * @author Vladimir Kondratyev |
| */ |
| public final class InplaceEditingLayer extends JComponent{ |
| private static final Logger LOG = Logger.getInstance("#com.intellij.uiDesigner.InplaceEditingLayer"); |
| |
| private final GuiEditor myEditor; |
| /** |
| * Trackes focus movements inside myInplaceEditorComponent |
| */ |
| private final MyFocusWatcher myFocusWatcher; |
| /** |
| * Commits or cancels editing |
| */ |
| private final MyPropertyEditorListener myPropertyEditorListener; |
| /** |
| * The component which is currently edited with inplace editor. |
| * This component can be null. |
| */ |
| private RadComponent myInplaceComponent; |
| /** |
| * Currently edited inplace property |
| */ |
| private Property myInplaceProperty; |
| /** |
| * Current inplace editor |
| */ |
| private PropertyEditor myInplaceEditor; |
| /** |
| * JComponent which is used as inplace editor |
| */ |
| private JComponent myInplaceEditorComponent; |
| /** |
| * Preferred bounds of the inplace editor component |
| */ |
| private Rectangle myPreferredBounds; |
| /** |
| * If <code>true</code> then we do not have to react on own events |
| */ |
| private boolean myInsideChange; |
| |
| public InplaceEditingLayer(@NotNull final GuiEditor editor) { |
| myEditor = editor; |
| myEditor.addComponentSelectionListener(new MyComponentSelectionListener()); |
| myFocusWatcher = new MyFocusWatcher(); |
| myPropertyEditorListener = new MyPropertyEditorListener(); |
| } |
| |
| /** |
| * This is optimization. We do not need to invalidate Swing hierarchy |
| * upper than InplaceEditingLayer. |
| */ |
| public boolean isValidateRoot() { |
| return true; |
| } |
| |
| /** |
| * When there is an inplace editor we "listen" all mouse event |
| * and finish editing by any MOUSE_PRESSED or MOUSE_RELEASED event. |
| * We are acting like yet another glass pane over the standard glass layer. |
| */ |
| protected void processMouseEvent(final MouseEvent e) { |
| if( |
| myInplaceComponent != null && |
| (MouseEvent.MOUSE_PRESSED == e.getID() || MouseEvent.MOUSE_RELEASED == e.getID()) |
| ){ |
| finishInplaceEditing(); |
| } |
| // [vova] this is very important! Without this code Swing doen't close popup menu on our |
| // layered pane. Swing adds MouseListeners to all component to close popup. If we do not |
| // invoke super then we lock all mouse listeners. |
| super.processMouseEvent(e); |
| } |
| |
| /** |
| * @return whether the layer is in "editing" state or not |
| */ |
| public boolean isEditing(){ |
| return myInplaceComponent != null; |
| } |
| |
| /** |
| * Starts editing of "inplace" property for the component at the |
| * specified point <code>(x, y)</code>. |
| * |
| * @param x x coordinate in the editor coordinate system |
| * @param y y coordinate in the editor coordinate system |
| */ |
| public void startInplaceEditing(final int x, final int y){ |
| final RadComponent inplaceComponent = FormEditingUtil.getRadComponentAt(myEditor.getRootContainer(), x, y); |
| if(inplaceComponent == null){ // nothing to edit |
| return; |
| } |
| |
| // Try to find property with inplace editor |
| final Point p = SwingUtilities.convertPoint(this, x, y, inplaceComponent.getDelegee()); |
| final Property inplaceProperty = inplaceComponent.getInplaceProperty(p.x, p.y); |
| if (inplaceProperty != null) { |
| final Rectangle bounds = inplaceComponent.getInplaceEditorBounds(inplaceProperty, p.x, p.y); |
| startInplaceEditing(inplaceComponent, inplaceProperty, bounds, new InplaceContext(true)); |
| } |
| } |
| |
| public void startInplaceEditing(@NotNull final RadComponent inplaceComponent, |
| @Nullable final Property property, |
| @Nullable final Rectangle bounds, |
| final InplaceContext context) { |
| myInplaceProperty = property; |
| if(myInplaceProperty == null){ |
| return; |
| } |
| |
| if (!myEditor.ensureEditable()) { |
| myInplaceProperty = null; |
| return; |
| } |
| |
| // Now we have to cancel previous inplace editing (if any) |
| |
| // Start new inplace editing |
| myInplaceComponent = inplaceComponent; |
| myInplaceEditor = myInplaceProperty.getEditor(); |
| LOG.assertTrue(myInplaceEditor != null); |
| |
| // 1. Get editor component |
| myInplaceEditorComponent = myInplaceEditor.getComponent( |
| myInplaceComponent, |
| context.isKeepInitialValue() ? myInplaceProperty.getValue(myInplaceComponent) : null, |
| context |
| ); |
| |
| if (context.isModalDialogDisplayed()) { // ListModel, for example |
| finishInplaceEditing(); |
| return; |
| } |
| |
| LOG.assertTrue(myInplaceEditorComponent != null); |
| myInplaceEditor.addPropertyEditorListener(myPropertyEditorListener); |
| |
| // 2. Set editor component bounds |
| final Dimension prefSize = myInplaceEditorComponent.getPreferredSize(); |
| if(bounds != null){ // use bounds provided by the component itself |
| final Point _p = SwingUtilities.convertPoint(myInplaceComponent.getDelegee(), bounds.x, bounds.y, this); |
| myPreferredBounds = new Rectangle(_p.x, _p.y, bounds.width, bounds.height); |
| } |
| else{ // set some default bounds |
| final Point _p = SwingUtilities.convertPoint(myInplaceComponent.getDelegee(), 0, 0, this); |
| myPreferredBounds = new Rectangle(_p.x, _p.y, myInplaceComponent.getWidth(), myInplaceComponent.getHeight()); |
| } |
| myInplaceEditorComponent.setBounds( |
| myPreferredBounds.x, |
| myPreferredBounds.y + (myPreferredBounds.height - prefSize.height)/2, |
| Math.min(Math.max(prefSize.width, myPreferredBounds.width), getWidth() - myPreferredBounds.x), |
| prefSize.height |
| ); |
| |
| // 3. Add it into layer |
| add(myInplaceEditorComponent); |
| myInplaceEditorComponent.revalidate(); |
| |
| // 4. Request focus into proper component |
| JComponent componentToFocus = myInplaceEditor.getPreferredFocusedComponent(myInplaceEditorComponent); |
| if (componentToFocus == null) { |
| componentToFocus = IdeFocusTraversalPolicy.getPreferredFocusedComponent(myInplaceEditorComponent); |
| } |
| if (componentToFocus == null) { |
| componentToFocus = myInplaceEditorComponent; |
| } |
| if (componentToFocus.requestFocusInWindow()) { |
| myFocusWatcher.install(myInplaceEditorComponent); |
| } |
| else { |
| grabFocus(); |
| final JComponent finalComponentToFocus = componentToFocus; |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| public void run() { |
| finalComponentToFocus.requestFocusInWindow(); |
| myFocusWatcher.install(myInplaceEditorComponent); |
| } |
| }); |
| } |
| |
| // 5. Block any mouse event to finish editing by any of them |
| enableEvents(MouseEvent.MOUSE_EVENT_MASK); |
| |
| repaint(); |
| } |
| |
| private void adjustEditorComponentSize(){ |
| if (myInplaceEditorComponent == null) return; |
| final Dimension preferredSize = myInplaceEditorComponent.getPreferredSize(); |
| int width = Math.max(preferredSize.width, myPreferredBounds.width); |
| // Editor component should not be extended to invisible area |
| width = Math.min(width, getWidth() - myInplaceEditorComponent.getX()); |
| myInplaceEditorComponent.setSize(width, myInplaceEditorComponent.getHeight()); |
| myInplaceEditorComponent.revalidate(); |
| } |
| |
| /** |
| * Finishes current inplace editing |
| */ |
| private void finishInplaceEditing(){ |
| if (myInplaceComponent == null || myInsideChange) { // nothing to finish |
| return; |
| } |
| myInsideChange = true; |
| try{ |
| // 1. Apply new value to the component |
| LOG.assertTrue(myInplaceEditor != null); |
| if (!myEditor.isUndoRedoInProgress()) { |
| CommandProcessor.getInstance().executeCommand( |
| myInplaceComponent.getProject(), |
| new Runnable() { |
| public void run() { |
| try { |
| final Object value = myInplaceEditor.getValue(); |
| myInplaceProperty.setValue(myInplaceComponent, value); |
| } |
| catch (Exception ignored) { |
| } |
| myEditor.refreshAndSave(true); |
| } |
| }, UIDesignerBundle.message("command.set.property.value"), null); |
| } |
| // 2. Remove editor from the layer |
| |
| if (myInplaceEditorComponent != null) { // reenterability guard |
| removeInplaceEditorComponent(); |
| myFocusWatcher.deinstall(myInplaceEditorComponent); |
| } |
| |
| myInplaceEditor.removePropertyEditorListener(myPropertyEditorListener); |
| |
| myInplaceComponent = null; |
| myInplaceEditorComponent = null; |
| myInplaceComponent = null; |
| |
| // 3. Let AWT work |
| disableEvents(MouseEvent.MOUSE_EVENT_MASK); |
| }finally{ |
| myInsideChange = false; |
| } |
| |
| repaint(); |
| } |
| |
| /** |
| * Cancells current inplace editing |
| */ |
| private void cancelInplaceEditing(){ |
| if(myInplaceComponent == null || myInsideChange){ // nothing to finish |
| return; |
| } |
| myInsideChange = true; |
| try{ |
| // 1. Remove editor from the layer |
| LOG.assertTrue(myInplaceProperty != null); |
| LOG.assertTrue(myInplaceEditor != null); |
| |
| removeInplaceEditorComponent(); |
| |
| myInplaceEditor.removePropertyEditorListener(myPropertyEditorListener); |
| myFocusWatcher.deinstall(myInplaceEditorComponent); |
| |
| myInplaceComponent = null; |
| myInplaceEditorComponent = null; |
| myInplaceComponent = null; |
| |
| // 2. Let AWT work |
| disableEvents(MouseEvent.MOUSE_EVENT_MASK); |
| }finally{ |
| myInsideChange = false; |
| } |
| |
| repaint(); |
| } |
| |
| private void removeInplaceEditorComponent() { |
| // [vova] before removing component from Swing tree we have to |
| // request component into glass layer. Otherwise focus from component being removed |
| // can go to some RadComponent. |
| |
| LayoutFocusTraversalPolicyExt.setOverridenDefaultComponent(myEditor.getGlassLayer()); |
| try { |
| remove(myInplaceEditorComponent); |
| } |
| finally { |
| LayoutFocusTraversalPolicyExt.setOverridenDefaultComponent(null); |
| } |
| } |
| |
| /** |
| * Finish inplace editing when selection changes |
| */ |
| private final class MyComponentSelectionListener implements ComponentSelectionListener{ |
| public void selectedComponentChanged(final GuiEditor source) { |
| finishInplaceEditing(); |
| } |
| } |
| |
| /** |
| * Finish inplace editing when inplace editor component loses focus |
| */ |
| private final class MyFocusWatcher extends FocusWatcher{ |
| protected void focusLostImpl(final FocusEvent e) { |
| final Component opposite = e.getOppositeComponent(); |
| if( |
| e.isTemporary() || |
| opposite != null && SwingUtilities.isDescendingFrom(opposite, getTopComponent()) |
| ){ |
| // Do nothing if focus moves inside top component hierarchy |
| return; |
| } |
| // [vova] we need LaterInvocator here to prevent write-access assertions |
| ApplicationManager.getApplication().invokeLater(new Runnable() { |
| public void run() { |
| finishInplaceEditing(); |
| } |
| }, ModalityState.NON_MODAL); |
| } |
| } |
| |
| /** |
| * Finishes editing by "Enter" and cancels editing by "Esc" |
| */ |
| private final class MyPropertyEditorListener extends PropertyEditorAdapter{ |
| public void valueCommitted(final PropertyEditor source, final boolean continueEditing, final boolean closeEditorOnError) { |
| finishInplaceEditing(); |
| } |
| |
| public void editingCanceled(final PropertyEditor source) { |
| cancelInplaceEditing(); |
| } |
| |
| public void preferredSizeChanged(final PropertyEditor source) { |
| adjustEditorComponentSize(); |
| } |
| } |
| } |