blob: 06dbad60f37a6700a2c5f321dca8cd663a50606a [file] [log] [blame]
/*
* 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();
}
}
}