IDEA-34482
[idea/community.git] / plugins / ui-designer / src / com / intellij / uiDesigner / designSurface / GuiEditor.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.codeInsight.daemon.DaemonCodeAnalyzer;
19 import com.intellij.ide.DeleteProvider;
20 import com.intellij.ide.palette.PaletteDragEventListener;
21 import com.intellij.ide.palette.impl.PaletteManager;
22 import com.intellij.lang.properties.psi.PropertiesFile;
23 import com.intellij.openapi.actionSystem.*;
24 import com.intellij.openapi.application.ApplicationManager;
25 import com.intellij.openapi.application.ModalityState;
26 import com.intellij.openapi.command.CommandProcessor;
27 import com.intellij.openapi.command.undo.UndoManager;
28 import com.intellij.openapi.diagnostic.Logger;
29 import com.intellij.openapi.editor.Document;
30 import com.intellij.openapi.editor.event.DocumentAdapter;
31 import com.intellij.openapi.editor.event.DocumentEvent;
32 import com.intellij.openapi.fileEditor.FileDocumentManager;
33 import com.intellij.openapi.fileTypes.StdFileTypes;
34 import com.intellij.openapi.module.Module;
35 import com.intellij.openapi.project.Project;
36 import com.intellij.openapi.util.Ref;
37 import com.intellij.openapi.vfs.ReadonlyStatusHandler;
38 import com.intellij.openapi.vfs.VirtualFile;
39 import com.intellij.psi.*;
40 import com.intellij.ui.ScrollPaneFactory;
41 import com.intellij.uiDesigner.*;
42 import com.intellij.uiDesigner.compiler.Utils;
43 import com.intellij.uiDesigner.componentTree.ComponentPtr;
44 import com.intellij.uiDesigner.componentTree.ComponentSelectionListener;
45 import com.intellij.uiDesigner.core.GridLayoutManager;
46 import com.intellij.uiDesigner.core.Util;
47 import com.intellij.uiDesigner.lw.CompiledClassPropertiesProvider;
48 import com.intellij.uiDesigner.lw.IComponent;
49 import com.intellij.uiDesigner.lw.IProperty;
50 import com.intellij.uiDesigner.lw.LwRootContainer;
51 import com.intellij.uiDesigner.palette.ComponentItem;
52 import com.intellij.uiDesigner.propertyInspector.PropertyInspector;
53 import com.intellij.uiDesigner.propertyInspector.UIDesignerToolWindowManager;
54 import com.intellij.uiDesigner.propertyInspector.properties.IntroStringProperty;
55 import com.intellij.uiDesigner.radComponents.RadComponent;
56 import com.intellij.uiDesigner.radComponents.RadContainer;
57 import com.intellij.uiDesigner.radComponents.RadRootContainer;
58 import com.intellij.uiDesigner.radComponents.RadTabbedPane;
59 import com.intellij.util.Alarm;
60 import org.jetbrains.annotations.NonNls;
61 import org.jetbrains.annotations.NotNull;
62 import org.jetbrains.annotations.Nullable;
63
64 import javax.swing.*;
65 import javax.swing.event.EventListenerList;
66 import javax.swing.event.ListSelectionEvent;
67 import javax.swing.event.ListSelectionListener;
68 import java.awt.*;
69 import java.awt.dnd.DnDConstants;
70 import java.awt.dnd.DropTarget;
71 import java.awt.event.*;
72 import java.lang.reflect.InvocationTargetException;
73 import java.util.HashMap;
74 import java.util.Locale;
75 import java.util.Map;
76
77 /**
78  * <code>GuiEditor</code> is a panel with border layout. It has palette at the north,
79  * tree of component with property editor at the west and editor area at the center.
80  * This editor area contains internal component where user edit the UI.
81  *
82  * @author Anton Katilin
83  * @author Vladimir Kondratyev
84  */
85 public final class GuiEditor extends JPanel implements DataProvider {
86   private static final Logger LOG = Logger.getInstance("#com.intellij.uiDesigner.GuiEditor");
87
88   @NotNull private final Module myModule;
89   @NotNull private final VirtualFile myFile;
90
91   /**
92    * for debug purposes
93    */
94   private Exception myWhere;
95
96   /**
97    * All component are on this layer
98    */
99   private static final Integer LAYER_COMPONENT = JLayeredPane.DEFAULT_LAYER;
100   /**
101    * This layer contains all "passive" decorators such as component boundaries
102    * and selection rectangle.
103    */
104   private static final Integer LAYER_PASSIVE_DECORATION = JLayeredPane.POPUP_LAYER;
105   /**
106    * We show (and move) dragged component at this layer
107    */
108   private static final Integer LAYER_DND = JLayeredPane.DRAG_LAYER;
109   /**
110    * This is the topmost layer. It gets and redispatch all incoming events
111    */
112   private static final Integer LAYER_GLASS = new Integer(JLayeredPane.DRAG_LAYER.intValue() + 100);
113   /**
114    * This layer contains all "active" decorators. This layer should be over
115    * LAYER_GLASS because active decorators must get AWT events to work correctly.
116    */
117   private static final Integer LAYER_ACTIVE_DECORATION = new Integer(LAYER_GLASS.intValue() + 100);
118   /**
119    * This layer contains all inplace editors.
120    */
121   private static final Integer LAYER_INPLACE_EDITING = new Integer(LAYER_ACTIVE_DECORATION.intValue() + 100);
122
123   private final EventListenerList myListenerList;
124   /**
125    * we have to store document here but not file because there can be a situation when
126    * document we added listener to has been disposed, and remove listener will be applied to
127    * a new document (got by file) -> assertion (see SCR 14143)
128    */
129   private final Document myDocument;
130
131   final MainProcessor myProcessor;
132   /**
133    * This layered pane contains all layers to lay components out and to
134    * show all necessary decoration items
135    */
136   @NotNull private final MyLayeredPane myLayeredPane;
137   /**
138    * The component which represents decoration layer. All passive
139    * decorators are on this layer.
140    */
141   private final PassiveDecorationLayer myDecorationLayer;
142   /**
143    * The component which represents layer where located all dragged
144    * components
145    */
146   private final DragLayer myDragLayer;
147   /**
148    * This layer contains all inplace editors
149    */
150   private final InplaceEditingLayer myInplaceEditingLayer;
151   /**
152    * Brings functionality to "DEL" button
153    */
154   private final MyDeleteProvider myDeleteProvider;
155   /**
156    * Rerun error analizer
157    */
158   private final MyPsiTreeChangeListener myPsiTreeChangeListener;
159
160   private RadRootContainer myRootContainer;
161   /**
162    * Panel with components palette.
163    */
164   //@NotNull private final PalettePanel myPalettePanel;
165   /**
166    * GuiEditor should not react on own events. If <code>myInsideChange</code>
167    * is <code>true</code> then we do not react on incoming DocumentEvent.
168    */
169   private boolean myInsideChange;
170   private final DocumentAdapter myDocumentListener;
171   private final CardLayout myCardLayout;
172
173   @NonNls private final static String CARD_VALID = "valid";
174   @NonNls private final static String CARD_INVALID = "invalid";
175   private final JPanel myValidCard;
176   private final JPanel myInvalidCard;
177   private boolean myInvalid = false;
178
179   private final CutCopyPasteSupport myCutCopyPasteSupport;
180   /**
181    * Implementation of Crtl+W and Ctrl+Shift+W behavior
182    */
183   private final SelectionState mySelectionState;
184   @NotNull private final GlassLayer myGlassLayer;
185   private final ActiveDecorationLayer myActiveDecorationLayer;
186
187   private boolean myShowGrid = true;
188   private boolean myShowComponentTags = true;
189   private final DesignDropTargetListener myDropTargetListener;
190   private JLabel myFormInvalidLabel;
191   private final QuickFixManagerImpl myQuickFixManager;
192   private final GridCaptionPanel myHorzCaptionPanel;
193   private final GridCaptionPanel myVertCaptionPanel;
194   private final MyPaletteKeyListener myPaletteKeyListener;
195   private final MyPaletteDragListener myPaletteDragListener;
196   private final MyPaletteSelectionListener myPaletteSelectionListener;
197   private ComponentPtr mySelectionAnchor;
198   private ComponentPtr mySelectionLead;
199   /**
200    * Undo group ID for undoing actions that need to be undone together with the form modification.
201    */
202   private Object myNextSaveGroupId = new Object();
203
204   @NonNls private static final String ourHelpID = "guiDesigner.uiTour.workspace";
205
206   public static final DataKey<GuiEditor> DATA_KEY = DataKey.create(GuiEditor.class.getName());
207
208   /**
209    * @param file file to be edited
210    * @throws java.lang.IllegalArgumentException
211    *          if the <code>file</code>
212    *          is <code>null</code> or <code>file</code> is not falid PsiFile
213    */
214   public GuiEditor(@NotNull final Module module, @NotNull final VirtualFile file) {
215     ApplicationManager.getApplication().assertIsDispatchThread();
216     LOG.assertTrue(file.isValid());
217
218     myModule = module;
219     myFile = file;
220
221     myCutCopyPasteSupport = new CutCopyPasteSupport(this);
222
223     myCardLayout = new CardLayout();
224     setLayout(myCardLayout);
225
226     myValidCard = new JPanel(new BorderLayout());
227     myInvalidCard = createInvalidCard();
228     add(myValidCard, CARD_VALID);
229     add(myInvalidCard, CARD_INVALID);
230
231     myListenerList = new EventListenerList();
232
233     myDecorationLayer = new PassiveDecorationLayer(this);
234     myDragLayer = new DragLayer(this);
235
236     myLayeredPane = new MyLayeredPane();
237     myInplaceEditingLayer = new InplaceEditingLayer(this);
238     myLayeredPane.add(myInplaceEditingLayer, LAYER_INPLACE_EDITING);
239     myActiveDecorationLayer = new ActiveDecorationLayer(this);
240     myLayeredPane.add(myActiveDecorationLayer, LAYER_ACTIVE_DECORATION);
241     myGlassLayer = new GlassLayer(this);
242     myLayeredPane.add(myGlassLayer, LAYER_GLASS);
243     myLayeredPane.add(myDecorationLayer, LAYER_PASSIVE_DECORATION);
244     myLayeredPane.add(myDragLayer, LAYER_DND);
245
246     myGlassLayer.addFocusListener(new FocusListener() {
247       public void focusGained(FocusEvent e) {
248         myDecorationLayer.repaint();
249         fireSelectedComponentChanged();
250       }
251
252       public void focusLost(FocusEvent e) {
253         myDecorationLayer.repaint();
254       }
255     });
256
257     // Ctrl+W / Ctrl+Shift+W support
258     mySelectionState = new SelectionState(this);
259
260     // DeleteProvider
261     myDeleteProvider = new MyDeleteProvider();
262
263     // We need to synchronize GUI editor with the document
264     final Alarm alarm = new Alarm();
265     myDocumentListener = new DocumentAdapter() {
266       public void documentChanged(final DocumentEvent e) {
267         if (!myInsideChange) {
268           UndoManager undoManager = UndoManager.getInstance(module.getProject());
269           alarm.cancelAllRequests();
270           alarm.addRequest(new MySynchronizeRequest(module, undoManager.isUndoInProgress() || undoManager.isRedoInProgress()),
271                            100/*any arbitrary delay*/, ModalityState.stateForComponent(GuiEditor.this));
272         }
273       }
274     };
275
276     // Prepare document
277     myDocument = FileDocumentManager.getInstance().getDocument(file);
278     myDocument.addDocumentListener(myDocumentListener);
279
280     // Read form from file
281     readFromFile(false);
282
283     JPanel panel = new JPanel(new GridBagLayout());
284     panel.setBackground(Color.LIGHT_GRAY);
285
286     myHorzCaptionPanel = new GridCaptionPanel(this, false);
287     myVertCaptionPanel = new GridCaptionPanel(this, true);
288
289     GridBagConstraints gbc = new GridBagConstraints();
290     gbc.gridx = 0;
291     gbc.gridy = 1;
292     gbc.weightx = 0.0;
293     gbc.weighty = 0.0;
294     gbc.fill = GridBagConstraints.BOTH;
295     panel.add(myVertCaptionPanel, gbc);
296
297     gbc.gridx = 1;
298     gbc.gridy = 0;
299     panel.add(myHorzCaptionPanel, gbc);
300
301     gbc.gridx = 1;
302     gbc.gridy = 1;
303     gbc.weightx = 1.0;
304     gbc.weighty = 1.0;
305     final JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(myLayeredPane);
306     scrollPane.setBackground(Color.WHITE);
307     panel.add(scrollPane, gbc);
308     myHorzCaptionPanel.attachToScrollPane(scrollPane);
309     myVertCaptionPanel.attachToScrollPane(scrollPane);
310
311     myValidCard.add(panel, BorderLayout.CENTER);
312
313     final CancelCurrentOperationAction cancelCurrentOperationAction = new CancelCurrentOperationAction();
314     cancelCurrentOperationAction.registerCustomShortcutSet(CommonShortcuts.ESCAPE, this);
315
316     myProcessor = new MainProcessor(this);
317
318     // PSI listener to restart error highlighter
319     myPsiTreeChangeListener = new MyPsiTreeChangeListener();
320     PsiManager.getInstance(module.getProject()).addPsiTreeChangeListener(myPsiTreeChangeListener);
321
322     myQuickFixManager = new QuickFixManagerImpl(this, myGlassLayer, scrollPane.getViewport());
323
324     myDropTargetListener = new DesignDropTargetListener(this);
325     if (!ApplicationManager.getApplication().isHeadlessEnvironment()) {
326       new DropTarget(getGlassLayer(), DnDConstants.ACTION_COPY_OR_MOVE, myDropTargetListener);
327     }
328
329     myActiveDecorationLayer.installSelectionWatcher();
330
331     final PaletteManager paletteManager = PaletteManager.getInstance(getProject());
332     myPaletteKeyListener = new MyPaletteKeyListener();
333     paletteManager.addKeyListener(myPaletteKeyListener);
334     myPaletteDragListener = new MyPaletteDragListener();
335     paletteManager.addDragEventListener(myPaletteDragListener);
336     myPaletteSelectionListener = new MyPaletteSelectionListener();
337     paletteManager.addSelectionListener(myPaletteSelectionListener);
338
339     ActionManager.getInstance().getAction("GuiDesigner.IncreaseIndent").registerCustomShortcutSet(
340       new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0)), myGlassLayer);
341     ActionManager.getInstance().getAction("GuiDesigner.DecreaseIndent").registerCustomShortcutSet(
342       new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, KeyEvent.SHIFT_MASK)), myGlassLayer);
343   }
344
345   @NotNull
346   public SelectionState getSelectionState() {
347     return mySelectionState;
348   }
349
350   public void dispose() {
351     ApplicationManager.getApplication().assertIsDispatchThread();
352
353     if (myWhere != null) {
354       LOG.error("Already disposed: old trace: ", myWhere);
355       LOG.error("Already disposed: new trace: ");
356     }
357     else {
358       myWhere = new Exception();
359     }
360
361     final PaletteManager paletteManager = PaletteManager.getInstance(getProject());
362     paletteManager.removeKeyListener(myPaletteKeyListener);
363     paletteManager.removeDragEventListener(myPaletteDragListener);
364     paletteManager.removeSelectionListener(myPaletteSelectionListener);
365     myDocument.removeDocumentListener(myDocumentListener);
366     PsiManager.getInstance(myModule.getProject()).removePsiTreeChangeListener(myPsiTreeChangeListener);
367     myPsiTreeChangeListener.dispose();
368   }
369
370   @NotNull
371   public Project getProject() {
372     return myModule.getProject();
373   }
374
375   @NotNull
376   public Module getModule() {
377     return myModule;
378   }
379
380   @NotNull
381   public VirtualFile getFile() {
382     return myFile;
383   }
384
385   public PsiFile getPsiFile() {
386     return PsiManager.getInstance(getProject()).findFile(myFile);
387   }
388
389   public boolean isEditable() {
390     final Document document = FileDocumentManager.getInstance().getDocument(myFile);
391     return document != null && document.isWritable();
392   }
393
394   public boolean ensureEditable() {
395     if (isEditable()) {
396       return true;
397     }
398     VirtualFile sourceFileToCheckOut = null;
399     if (!GuiDesignerConfiguration.getInstance(getProject()).INSTRUMENT_CLASSES) {
400       final String classToBind = myRootContainer.getClassToBind();
401       if (classToBind != null && classToBind.length() > 0) {
402         PsiClass psiClass = FormEditingUtil.findClassToBind(myModule, classToBind);
403         if (psiClass != null) {
404           sourceFileToCheckOut = psiClass.getContainingFile().getVirtualFile();
405         }
406       }
407     }
408
409     final ReadonlyStatusHandler.OperationStatus status;
410     if (sourceFileToCheckOut != null) {
411       status = ReadonlyStatusHandler.getInstance(getProject()).ensureFilesWritable(myFile, sourceFileToCheckOut);
412     }
413     else {
414       status = ReadonlyStatusHandler.getInstance(getProject()).ensureFilesWritable(myFile);
415     }
416     return !status.hasReadonlyFiles();
417   }
418
419   public void refresh() {
420     refreshImpl(myRootContainer);
421     myRootContainer.getDelegee().revalidate();
422     repaintLayeredPane();
423   }
424
425   public void refreshAndSave(final boolean forceSync) {
426     // Update property inspector
427     final UIDesignerToolWindowManager manager = UIDesignerToolWindowManager.getInstance(getProject());
428     final PropertyInspector propertyInspector = manager.getPropertyInspector();
429     if (propertyInspector != null) {
430       propertyInspector.synchWithTree(forceSync);
431     }
432
433     refresh();
434     saveToFile();
435     // TODO[yole]: install appropriate listeners so that the captions repaint themselves at correct time
436     myHorzCaptionPanel.repaint();
437     myVertCaptionPanel.repaint();
438   }
439
440   public Object getNextSaveGroupId() {
441     return myNextSaveGroupId;
442   }
443
444   private static void refreshImpl(final RadComponent component) {
445     if (component.getParent() != null) {
446       final Dimension size = component.getSize();
447       final int oldWidth = size.width;
448       final int oldHeight = size.height;
449       Util.adjustSize(component.getDelegee(), component.getConstraints(), size);
450
451       if (oldWidth != size.width || oldHeight != size.height) {
452         if (component.getParent().isXY()) {
453           component.setSize(size);
454         }
455         component.getDelegee().invalidate();
456       }
457     }
458
459     if (component instanceof RadContainer) {
460       component.refresh();
461
462       final RadContainer container = (RadContainer)component;
463       for (int i = container.getComponentCount() - 1; i >= 0; i--) {
464         refreshImpl(container.getComponent(i));
465       }
466     }
467   }
468
469   public Object getData(final String dataId) {
470     if (PlatformDataKeys.HELP_ID.is(dataId)) {
471       return ourHelpID;
472     }
473
474     // Standard Swing cut/copy/paste actions should work if user is editing something inside property inspector
475     final UIDesignerToolWindowManager manager = UIDesignerToolWindowManager.getInstance(getProject());
476     final PropertyInspector inspector = manager.getPropertyInspector();
477     if (inspector != null && inspector.isEditing()) {
478       return null;
479     }
480
481     if (PlatformDataKeys.DELETE_ELEMENT_PROVIDER.is(dataId)) {
482       return myDeleteProvider;
483     }
484
485     if (PlatformDataKeys.COPY_PROVIDER.is(dataId) ||
486         PlatformDataKeys.CUT_PROVIDER.is(dataId) ||
487         PlatformDataKeys.PASTE_PROVIDER.is(dataId)) {
488       return myCutCopyPasteSupport;
489     }
490
491     return null;
492   }
493
494   private JPanel createInvalidCard() {
495     final JPanel panel = new JPanel(new GridBagLayout());
496     myFormInvalidLabel = new JLabel(UIDesignerBundle.message("error.form.file.is.invalid"));
497     panel.add(myFormInvalidLabel,
498               new GridBagConstraints(0, 0, 1, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
499     return panel;
500   }
501
502   /**
503    * @return the component which represents DnD layer. All currently
504    *         dragged (moved) component are on this layer.
505    */
506   public DragLayer getDragLayer() {
507     return myDragLayer;
508   }
509
510   /**
511    * @return the topmost <code>UiConainer</code> which in the root of
512    *         component hierarchy. This method never returns <code>null</code>.
513    */
514   @NotNull
515   public RadRootContainer getRootContainer() {
516     return myRootContainer;
517   }
518
519   /**
520    * Fires event that selection changes
521    */
522   public void fireSelectedComponentChanged() {
523     final ComponentSelectionListener[] listeners = myListenerList.getListeners(ComponentSelectionListener.class);
524     for (ComponentSelectionListener listener : listeners) {
525       listener.selectedComponentChanged(this);
526     }
527   }
528
529   private void fireHierarchyChanged() {
530     final HierarchyChangeListener[] listeners = myListenerList.getListeners(HierarchyChangeListener.class);
531     for (final HierarchyChangeListener listener : listeners) {
532       listener.hierarchyChanged();
533     }
534   }
535
536   @NotNull
537   public GlassLayer getGlassLayer() {
538     return myGlassLayer;
539   }
540
541   /**
542    * @return the component which represents layer with active decorators
543    *         such as grid edit controls, inplace editors, etc.
544    */
545   public InplaceEditingLayer getInplaceEditingLayer() {
546     return myInplaceEditingLayer;
547   }
548
549   @NotNull
550   public JLayeredPane getLayeredPane() {
551     return myLayeredPane;
552   }
553
554   public void repaintLayeredPane() {
555     myLayeredPane.repaint();
556   }
557
558   /**
559    * Adds specified selection listener. This listener gets notification each time
560    * the selection in the component the changes.
561    */
562   public void addComponentSelectionListener(final ComponentSelectionListener l) {
563     myListenerList.add(ComponentSelectionListener.class, l);
564   }
565
566   /**
567    * Removes specified selection listener
568    */
569   public void removeComponentSelectionListener(final ComponentSelectionListener l) {
570     myListenerList.remove(ComponentSelectionListener.class, l);
571   }
572
573   /**
574    * Adds specified hierarchy change listener
575    */
576   public void addHierarchyChangeListener(@NotNull final HierarchyChangeListener l) {
577     myListenerList.add(HierarchyChangeListener.class, l);
578   }
579
580   /**
581    * Removes specified hierarchy change listener
582    */
583   public void removeHierarchyChangeListener(@NotNull final HierarchyChangeListener l) {
584     myListenerList.remove(HierarchyChangeListener.class, l);
585   }
586
587   private void saveToFile() {
588     LOG.debug("GuiEditor.saveToFile(): group ID=" + myNextSaveGroupId);
589     CommandProcessor.getInstance().executeCommand(myModule.getProject(), new Runnable() {
590       public void run() {
591         ApplicationManager.getApplication().runWriteAction(new Runnable() {
592           public void run() {
593             myInsideChange = true;
594             try {
595               final XmlWriter writer = new XmlWriter();
596               getRootContainer().write(writer);
597               final String newText = writer.getText();
598               final String oldText = myDocument.getText();
599
600               try {
601                 final ReplaceInfo replaceInfo = findFragmentToChange(oldText, newText);
602                 if (replaceInfo.getStartOffset() == -1) {
603                   // do nothing - texts are equal
604                 }
605                 else {
606                   myDocument.replaceString(replaceInfo.getStartOffset(), replaceInfo.getEndOffset(), replaceInfo.getReplacement());
607                 }
608               }
609               catch (Exception e) {
610                 LOG.error(e);
611                 myDocument.replaceString(0, oldText.length(), newText);
612               }
613             }
614             finally {
615               myInsideChange = false;
616             }
617           }
618         });
619       }
620     }, "UI Designer Save", myNextSaveGroupId);
621     myNextSaveGroupId = new Object();
622
623     fireHierarchyChanged();
624   }
625
626   public ActiveDecorationLayer getActiveDecorationLayer() {
627     return myActiveDecorationLayer;
628   }
629
630   public void setStringDescriptorLocale(final Locale locale) {
631     myRootContainer.setStringDescriptorLocale(locale);
632     refreshProperties();
633     UIDesignerToolWindowManager.getInstance(getProject()).updateComponentTree();
634     DaemonCodeAnalyzer.getInstance(getProject()).restart();
635   }
636
637   @Nullable
638   public Locale getStringDescriptorLocale() {
639     return myRootContainer.getStringDescriptorLocale();
640   }
641
642   private void refreshProperties() {
643     final Ref<Boolean> anythingModified = new Ref<Boolean>();
644     FormEditingUtil.iterate(myRootContainer, new FormEditingUtil.ComponentVisitor() {
645       public boolean visit(final IComponent component) {
646         final RadComponent radComponent = (RadComponent)component;
647         boolean componentModified = false;
648         for (IProperty prop : component.getModifiedProperties()) {
649           if (prop instanceof IntroStringProperty) {
650             IntroStringProperty strProp = (IntroStringProperty)prop;
651             componentModified = strProp.refreshValue(radComponent) || componentModified;
652           }
653         }
654
655         if (component instanceof RadContainer) {
656           componentModified = ((RadContainer)component).updateBorder() || componentModified;
657         }
658
659         if (component.getParentContainer() instanceof RadTabbedPane) {
660           componentModified = ((RadTabbedPane)component.getParentContainer()).refreshChildTitle(radComponent) || componentModified;
661         }
662         if (componentModified) {
663           anythingModified.set(Boolean.TRUE);
664         }
665
666         return true;
667       }
668     });
669     if (!anythingModified.isNull()) {
670       refresh();
671       final UIDesignerToolWindowManager twm = UIDesignerToolWindowManager.getInstance(getProject());
672       twm.getComponentTree().repaint();
673       twm.getPropertyInspector().synchWithTree(true);
674     }
675   }
676
677   public MainProcessor getMainProcessor() {
678     return myProcessor;
679   }
680
681   public void refreshIntentionHint() {
682     myQuickFixManager.refreshIntentionHint();
683   }
684
685   public void setSelectionAnchor(final RadComponent component) {
686     mySelectionAnchor = new ComponentPtr(this, component);
687   }
688
689   @Nullable
690   public RadComponent getSelectionAnchor() {
691     if (mySelectionAnchor == null) return null;
692     mySelectionAnchor.validate();
693     return mySelectionAnchor.getComponent();
694   }
695
696   public void setSelectionLead(final RadComponent component) {
697     mySelectionLead = new ComponentPtr(this, component);
698   }
699
700   @Nullable
701   public RadComponent getSelectionLead() {
702     if (mySelectionLead == null) return null;
703     mySelectionLead.validate();
704     return mySelectionLead.getComponent();
705   }
706
707   public void scrollComponentInView(final RadComponent component) {
708     Rectangle rect = SwingUtilities.convertRectangle(component.getDelegee().getParent(), component.getBounds(), myLayeredPane);
709     myLayeredPane.scrollRectToVisible(rect);
710   }
711
712   public static final class ReplaceInfo {
713     private final int myStartOffset;
714     private final int myEndOffset;
715     private final String myReplacement;
716
717     public ReplaceInfo(final int startOffset, final int endOffset, final String replacement) {
718       myStartOffset = startOffset;
719       myEndOffset = endOffset;
720       myReplacement = replacement;
721     }
722
723     public int getStartOffset() {
724       return myStartOffset;
725     }
726
727     public int getEndOffset() {
728       return myEndOffset;
729     }
730
731     public String getReplacement() {
732       return myReplacement;
733     }
734   }
735
736   public static ReplaceInfo findFragmentToChange(final String oldText, final String newText) {
737     if (oldText.equals(newText)) {
738       return new ReplaceInfo(-1, -1, null);
739     }
740
741     final int oldLength = oldText.length();
742     final int newLength = newText.length();
743
744     int startOffset = 0;
745     while (
746       startOffset < oldLength && startOffset < newLength &&
747       oldText.charAt(startOffset) == newText.charAt(startOffset)
748       ) {
749       startOffset++;
750     }
751
752     int endOffset = oldLength;
753     while (true) {
754       if (endOffset <= startOffset) {
755         break;
756       }
757       final int idxInNew = newLength - (oldLength - endOffset) - 1;
758       if (idxInNew < startOffset) {
759         break;
760       }
761
762       final char c1 = oldText.charAt(endOffset - 1);
763       final char c2 = newText.charAt(idxInNew);
764       if (c1 != c2) {
765         break;
766       }
767       endOffset--;
768     }
769
770     return new ReplaceInfo(startOffset, endOffset, newText.substring(startOffset, newLength - (oldLength - endOffset)));
771   }
772
773   /**
774    * @param rootContainer new container to be set as a root.
775    */
776   private void setRootContainer(@NotNull final RadRootContainer rootContainer) {
777     if (myRootContainer != null) {
778       myLayeredPane.remove(myRootContainer.getDelegee());
779     }
780     myRootContainer = rootContainer;
781     setDesignTimeInsets(2);
782     myLayeredPane.add(myRootContainer.getDelegee(), LAYER_COMPONENT);
783
784     fireHierarchyChanged();
785   }
786
787   public void setDesignTimeInsets(final int insets) {
788     Integer oldInsets = (Integer)myRootContainer.getDelegee().getClientProperty(GridLayoutManager.DESIGN_TIME_INSETS);
789     if (oldInsets == null || oldInsets.intValue() != insets) {
790       myRootContainer.getDelegee().putClientProperty(GridLayoutManager.DESIGN_TIME_INSETS, insets);
791       revalidateRecursive(myRootContainer.getDelegee());
792     }
793   }
794
795   private static void revalidateRecursive(final JComponent component) {
796     for (Component child : component.getComponents()) {
797       if (child instanceof JComponent) {
798         revalidateRecursive((JComponent)child);
799       }
800     }
801     component.revalidate();
802     component.repaint();
803   }
804
805   /**
806    * Creates and sets new <code>RadRootContainer</code>
807    *
808    * @param keepSelection if true, the GUI designer tries to preserve the selection state after reload.
809    */
810   public void readFromFile(final boolean keepSelection) {
811     try {
812       ComponentPtr[] selection = null;
813       Map<String, String> tabbedPaneSelectedTabs = null;
814       if (keepSelection) {
815         selection = SelectionState.getSelection(this);
816         tabbedPaneSelectedTabs = saveTabbedPaneSelectedTabs();
817       }
818       Locale oldLocale = null;
819       if (myRootContainer != null) {
820         oldLocale = myRootContainer.getStringDescriptorLocale();
821       }
822
823       final String text = myDocument.getText();
824
825       final ClassLoader classLoader = LoaderFactory.getInstance(myModule.getProject()).getLoader(myFile);
826
827       final LwRootContainer rootContainer = Utils.getRootContainer(text, new CompiledClassPropertiesProvider(classLoader));
828       final RadRootContainer container = XmlReader.createRoot(myModule, rootContainer, classLoader, oldLocale);
829       setRootContainer(container);
830       if (keepSelection) {
831         SelectionState.restoreSelection(this, selection);
832         restoreTabbedPaneSelectedTabs(tabbedPaneSelectedTabs);
833       }
834       myInvalid = false;
835       myCardLayout.show(this, CARD_VALID);
836       refresh();
837     }
838     catch (Exception exc) {
839       Throwable original = exc;
840       while (original instanceof InvocationTargetException) {
841         original = original.getCause();
842       }
843       showInvalidCard(original);
844     }
845     catch (final LinkageError exc) {
846       showInvalidCard(exc);
847     }
848   }
849
850   private void showInvalidCard(final Throwable exc) {
851     LOG.info(exc);
852     // setting fictive container
853     setRootContainer(new RadRootContainer(myModule, "0"));
854     myFormInvalidLabel.setText(UIDesignerBundle.message("error.form.file.is.invalid.message", FormEditingUtil.getExceptionMessage(exc)));
855     myInvalid = true;
856     myCardLayout.show(this, CARD_INVALID);
857     repaint();
858   }
859
860   public boolean isFormInvalid() {
861     return myInvalid;
862   }
863
864   private Map<String, String> saveTabbedPaneSelectedTabs() {
865     final Map<String, String> result = new HashMap<String, String>();
866     FormEditingUtil.iterate(getRootContainer(), new FormEditingUtil.ComponentVisitor() {
867       public boolean visit(final IComponent component) {
868         if (component instanceof RadTabbedPane) {
869           RadTabbedPane tabbedPane = (RadTabbedPane)component;
870           RadComponent c = tabbedPane.getSelectedTab();
871           if (c != null) {
872             result.put(tabbedPane.getId(), c.getId());
873           }
874         }
875         return true;
876       }
877     });
878     return result;
879   }
880
881   private void restoreTabbedPaneSelectedTabs(final Map<String, String> tabbedPaneSelectedTabs) {
882     FormEditingUtil.iterate(getRootContainer(), new FormEditingUtil.ComponentVisitor() {
883       public boolean visit(final IComponent component) {
884         if (component instanceof RadTabbedPane) {
885           RadTabbedPane tabbedPane = (RadTabbedPane)component;
886           String selectedTabId = tabbedPaneSelectedTabs.get(tabbedPane.getId());
887           if (selectedTabId != null) {
888             for (RadComponent c : tabbedPane.getComponents()) {
889               if (c.getId().equals(selectedTabId)) {
890                 tabbedPane.selectTab(c);
891                 break;
892               }
893             }
894           }
895         }
896         return true;
897       }
898     });
899   }
900
901   public JComponent getPreferredFocusedComponent() {
902     if (myValidCard.isVisible()) {
903       return myGlassLayer;
904     }
905     else {
906       return myInvalidCard;
907     }
908   }
909
910   public static void repaintLayeredPane(final RadComponent component) {
911     final GuiEditor uiEditor = (GuiEditor)SwingUtilities.getAncestorOfClass(GuiEditor.class, component.getDelegee());
912     if (uiEditor != null) {
913       uiEditor.repaintLayeredPane();
914     }
915   }
916
917   public boolean isShowGrid() {
918     return myShowGrid;
919   }
920
921   public void setShowGrid(final boolean showGrid) {
922     if (myShowGrid != showGrid) {
923       myShowGrid = showGrid;
924       repaint();
925     }
926   }
927
928   public boolean isShowComponentTags() {
929     return myShowComponentTags;
930   }
931
932   public void setShowComponentTags(final boolean showComponentTags) {
933     if (myShowComponentTags != showComponentTags) {
934       myShowComponentTags = showComponentTags;
935       repaint();
936     }
937   }
938
939   public DesignDropTargetListener getDropTargetListener() {
940     return myDropTargetListener;
941   }
942
943   @Nullable
944   public GridCaptionPanel getFocusedCaptionPanel() {
945     if (myHorzCaptionPanel.isFocusOwner()) {
946       return myHorzCaptionPanel;
947     }
948     else if (myVertCaptionPanel.isFocusOwner()) {
949       return myVertCaptionPanel;
950     }
951     return null;
952   }
953
954   private boolean isActiveEditor() {
955     return UIDesignerToolWindowManager.getInstance(getProject()).getActiveFormEditor() == this;
956   }
957
958   void hideIntentionHint() {
959     myQuickFixManager.hideIntentionHint();
960   }
961
962   private final class MyLayeredPane extends JLayeredPane implements Scrollable {
963     /**
964      * All components allocate whole pane's area.
965      */
966     public void doLayout() {
967       for (int i = getComponentCount() - 1; i >= 0; i--) {
968         final Component component = getComponent(i);
969         component.setBounds(0, 0, getWidth(), getHeight());
970       }
971     }
972
973     public Dimension getMinimumSize() {
974       return getPreferredSize();
975     }
976
977     public Dimension getPreferredSize() {
978       // make sure all components fit
979       int width = 0;
980       int height = 0;
981       for (int i = 0; i < myRootContainer.getComponentCount(); i++) {
982         final RadComponent component = myRootContainer.getComponent(i);
983         width = Math.max(width, component.getX() + component.getWidth());
984         height = Math.max(height, component.getY() + component.getHeight());
985       }
986
987       width += 50;
988       height += 40;
989
990       return new Dimension(width, height);
991     }
992
993     public Dimension getPreferredScrollableViewportSize() {
994       return getPreferredSize();
995     }
996
997     public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
998       return 10;
999     }
1000
1001     public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
1002       if (orientation == SwingConstants.HORIZONTAL) {
1003         return visibleRect.width - 10;
1004       }
1005       return visibleRect.height - 10;
1006     }
1007
1008     public boolean getScrollableTracksViewportWidth() {
1009       return false;
1010     }
1011
1012     public boolean getScrollableTracksViewportHeight() {
1013       return false;
1014     }
1015   }
1016
1017   /**
1018    * Action works only if we are not editing something in the property inspector
1019    */
1020   private final class CancelCurrentOperationAction extends AnAction {
1021     public void actionPerformed(final AnActionEvent e) {
1022       myProcessor.cancelOperation();
1023       myQuickFixManager.hideIntentionHint();
1024     }
1025
1026     public void update(final AnActionEvent e) {
1027       final UIDesignerToolWindowManager manager = UIDesignerToolWindowManager.getInstance(getProject());
1028       e.getPresentation().setEnabled(!manager.getPropertyInspector().isEditing());
1029     }
1030   }
1031
1032   /**
1033    * Allows "DEL" button to work through the standard mechanism
1034    */
1035   private final class MyDeleteProvider implements DeleteProvider {
1036     public void deleteElement(final DataContext dataContext) {
1037       if (!GuiEditor.this.ensureEditable()) {
1038         return;
1039       }
1040       CommandProcessor.getInstance().executeCommand(getProject(), new Runnable() {
1041         public void run() {
1042           FormEditingUtil.deleteSelection(GuiEditor.this);
1043         }
1044       }, UIDesignerBundle.message("command.delete.selection"), null);
1045     }
1046
1047     public boolean canDeleteElement(final DataContext dataContext) {
1048       final UIDesignerToolWindowManager manager = UIDesignerToolWindowManager.getInstance(getProject());
1049       return
1050         !manager.getPropertyInspector().isEditing() &&
1051         !myInplaceEditingLayer.isEditing() &&
1052         FormEditingUtil.canDeleteSelection(GuiEditor.this);
1053     }
1054   }
1055
1056   /**
1057    * Listens PSI event and update error highlighting in the UI editor
1058    */
1059   private final class MyPsiTreeChangeListener extends PsiTreeChangeAdapter {
1060     private final Alarm myAlarm;
1061     private final MyRefreshPropertiesRequest myRefreshPropertiesRequest = new MyRefreshPropertiesRequest();
1062     private final MySynchronizeRequest mySynchronizeRequest = new MySynchronizeRequest(myModule, true);
1063
1064     public MyPsiTreeChangeListener() {
1065       myAlarm = new Alarm();
1066     }
1067
1068     /**
1069      * Cancels all pending update requests. You have to cancel all pending requests
1070      * to not access to closed project.
1071      */
1072     public void dispose() {
1073       myAlarm.cancelAllRequests();
1074     }
1075
1076     public void childAdded(final PsiTreeChangeEvent event) {
1077       handleEvent(event);
1078     }
1079
1080     public void childMoved(final PsiTreeChangeEvent event) {
1081       handleEvent(event);
1082     }
1083
1084     public void childrenChanged(final PsiTreeChangeEvent event) {
1085       handleEvent(event);
1086     }
1087
1088     public void childRemoved(PsiTreeChangeEvent event) {
1089       handleEvent(event);
1090     }
1091
1092     public void childReplaced(PsiTreeChangeEvent event) {
1093       handleEvent(event);
1094     }
1095
1096     public void propertyChanged(final PsiTreeChangeEvent event) {
1097       if (PsiTreeChangeEvent.PROP_ROOTS.equals(event.getPropertyName())) {
1098         myAlarm.cancelRequest(myRefreshPropertiesRequest);
1099         myAlarm.addRequest(myRefreshPropertiesRequest, 500, ModalityState.stateForComponent(GuiEditor.this));
1100       }
1101     }
1102
1103     private void handleEvent(final PsiTreeChangeEvent event) {
1104       if (event.getParent() != null) {
1105         PsiFile containingFile = event.getParent().getContainingFile();
1106         if (containingFile instanceof PropertiesFile) {
1107           LOG.debug("Received PSI change event for properties file");
1108           myAlarm.cancelRequest(myRefreshPropertiesRequest);
1109           myAlarm.addRequest(myRefreshPropertiesRequest, 500, ModalityState.stateForComponent(GuiEditor.this));
1110         }
1111         else if (containingFile instanceof PsiPlainTextFile && containingFile.getFileType().equals(StdFileTypes.GUI_DESIGNER_FORM)) {
1112           // quick check if relevant
1113           String resourceName = FormEditingUtil.buildResourceName(containingFile);
1114           if (myDocument.getText().indexOf(resourceName) >= 0) {
1115             LOG.debug("Received PSI change event for nested form");
1116             // TODO[yole]: handle multiple nesting
1117             myAlarm.cancelRequest(mySynchronizeRequest);
1118             myAlarm.addRequest(mySynchronizeRequest, 500, ModalityState.stateForComponent(GuiEditor.this));
1119           }
1120         }
1121       }
1122     }
1123   }
1124
1125   private class MySynchronizeRequest implements Runnable {
1126     private final Module myModule;
1127     private final boolean myKeepSelection;
1128
1129     public MySynchronizeRequest(final Module module, final boolean keepSelection) {
1130       myModule = module;
1131       myKeepSelection = keepSelection;
1132     }
1133
1134     public void run() {
1135       LOG.debug("Synchronizing GUI editor " + myFile.getName() + " to document");
1136       PsiDocumentManager.getInstance(myModule.getProject()).commitDocument(myDocument);
1137       readFromFile(myKeepSelection);
1138     }
1139   }
1140
1141   private class MyRefreshPropertiesRequest implements Runnable {
1142     public void run() {
1143       if (!myModule.isDisposed() && !getProject().isDisposed()) {
1144         refreshProperties();
1145       }
1146     }
1147   }
1148
1149   private class MyPaletteKeyListener extends KeyAdapter {
1150     @Override
1151     public void keyPressed(KeyEvent e) {
1152       PaletteManager paletteManager = PaletteManager.getInstance(getProject());
1153       if (e.getKeyCode() == KeyEvent.VK_SHIFT && paletteManager.getActiveItem(ComponentItem.class) != null && isActiveEditor()) {
1154         setDesignTimeInsets(12);
1155       }
1156     }
1157
1158     @Override
1159     public void keyReleased(KeyEvent e) {
1160       if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
1161         setDesignTimeInsets(2);
1162       }
1163     }
1164   }
1165
1166   private class MyPaletteDragListener implements PaletteDragEventListener {
1167     public void dropActionChanged(int gestureModifiers) {
1168       if ((gestureModifiers & InputEvent.SHIFT_MASK) != 0 && isActiveEditor()) {
1169         setDesignTimeInsets(12);
1170       }
1171       else {
1172         setDesignTimeInsets(2);
1173       }
1174     }
1175   }
1176
1177   private class MyPaletteSelectionListener implements ListSelectionListener {
1178     public void valueChanged(ListSelectionEvent e) {
1179       if (PaletteManager.getInstance(getProject()).getActiveItem() == null) {
1180         myProcessor.cancelPaletteInsert();
1181       }
1182     }
1183   }
1184 }