00f71dfd6cce94cd333df3752b180634ccf6d2d4
[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       final RadContainer container = (RadContainer)component;
461       for (int i = container.getComponentCount() - 1; i >= 0; i--) {
462         refreshImpl(container.getComponent(i));
463       }
464     }
465   }
466
467   public Object getData(final String dataId) {
468     if (PlatformDataKeys.HELP_ID.is(dataId)) {
469       return ourHelpID;
470     }
471
472     // Standard Swing cut/copy/paste actions should work if user is editing something inside property inspector
473     final UIDesignerToolWindowManager manager = UIDesignerToolWindowManager.getInstance(getProject());
474     final PropertyInspector inspector = manager.getPropertyInspector();
475     if (inspector != null && inspector.isEditing()) {
476       return null;
477     }
478
479     if (PlatformDataKeys.DELETE_ELEMENT_PROVIDER.is(dataId)) {
480       return myDeleteProvider;
481     }
482
483     if (PlatformDataKeys.COPY_PROVIDER.is(dataId) ||
484         PlatformDataKeys.CUT_PROVIDER.is(dataId) ||
485         PlatformDataKeys.PASTE_PROVIDER.is(dataId)) {
486       return myCutCopyPasteSupport;
487     }
488
489     return null;
490   }
491
492   private JPanel createInvalidCard() {
493     final JPanel panel = new JPanel(new GridBagLayout());
494     myFormInvalidLabel = new JLabel(UIDesignerBundle.message("error.form.file.is.invalid"));
495     panel.add(myFormInvalidLabel,
496               new GridBagConstraints(0, 0, 1, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
497     return panel;
498   }
499
500   /**
501    * @return the component which represents DnD layer. All currently
502    *         dragged (moved) component are on this layer.
503    */
504   public DragLayer getDragLayer() {
505     return myDragLayer;
506   }
507
508   /**
509    * @return the topmost <code>UiConainer</code> which in the root of
510    *         component hierarchy. This method never returns <code>null</code>.
511    */
512   @NotNull
513   public RadRootContainer getRootContainer() {
514     return myRootContainer;
515   }
516
517   /**
518    * Fires event that selection changes
519    */
520   public void fireSelectedComponentChanged() {
521     final ComponentSelectionListener[] listeners = myListenerList.getListeners(ComponentSelectionListener.class);
522     for (ComponentSelectionListener listener : listeners) {
523       listener.selectedComponentChanged(this);
524     }
525   }
526
527   private void fireHierarchyChanged() {
528     final HierarchyChangeListener[] listeners = myListenerList.getListeners(HierarchyChangeListener.class);
529     for(final HierarchyChangeListener listener : listeners) {
530       listener.hierarchyChanged();
531     }
532   }
533
534   @NotNull
535   public GlassLayer getGlassLayer() {
536     return myGlassLayer;
537   }
538
539   /**
540    * @return the component which represents layer with active decorators
541    * such as grid edit controls, inplace editors, etc.
542    */
543   public InplaceEditingLayer getInplaceEditingLayer() {
544     return myInplaceEditingLayer;
545   }
546
547   @NotNull
548   public JLayeredPane getLayeredPane() {
549     return myLayeredPane;
550   }
551
552   public void repaintLayeredPane() {
553     myLayeredPane.repaint();
554   }
555
556   /**
557    * Adds specified selection listener. This listener gets notification each time
558    * the selection in the component the changes.
559    */
560   public void addComponentSelectionListener(final ComponentSelectionListener l) {
561     myListenerList.add(ComponentSelectionListener.class, l);
562   }
563
564   /**
565    * Removes specified selection listener
566    */
567   public void removeComponentSelectionListener(final ComponentSelectionListener l) {
568     myListenerList.remove(ComponentSelectionListener.class, l);
569   }
570
571   /**
572    * Adds specified hierarchy change listener
573    */
574   public void addHierarchyChangeListener(@NotNull final HierarchyChangeListener l) {
575     myListenerList.add(HierarchyChangeListener.class, l);
576   }
577
578   /**
579    * Removes specified hierarchy change listener
580    */
581   public void removeHierarchyChangeListener(@NotNull final HierarchyChangeListener l) {
582     myListenerList.remove(HierarchyChangeListener.class, l);
583   }
584
585   private void saveToFile() {
586     LOG.debug("GuiEditor.saveToFile(): group ID=" + myNextSaveGroupId);
587     CommandProcessor.getInstance().executeCommand(myModule.getProject(), new Runnable() {
588       public void run() {
589         ApplicationManager.getApplication().runWriteAction(new Runnable() {
590           public void run() {
591             myInsideChange = true;
592             try {
593               final XmlWriter writer = new XmlWriter();
594               getRootContainer().write(writer);
595               final String newText = writer.getText();
596               final String oldText = myDocument.getText();
597
598               try {
599                 final ReplaceInfo replaceInfo = findFragmentToChange(oldText, newText);
600                 if (replaceInfo.getStartOffset() == -1) {
601                   // do nothing - texts are equal
602                 }
603                 else {
604                   myDocument.replaceString(replaceInfo.getStartOffset(), replaceInfo.getEndOffset(), replaceInfo.getReplacement());
605                 }
606               }
607               catch (Exception e) {
608                 LOG.error(e);
609                 myDocument.replaceString(0, oldText.length(), newText);
610               }
611             }
612             finally {
613               myInsideChange = false;
614             }
615           }
616         });
617       }
618     }, "UI Designer Save", myNextSaveGroupId);
619     myNextSaveGroupId = new Object();
620
621     fireHierarchyChanged();
622   }
623
624   public ActiveDecorationLayer getActiveDecorationLayer() {
625     return myActiveDecorationLayer;
626   }
627
628   public void setStringDescriptorLocale(final Locale locale) {
629     myRootContainer.setStringDescriptorLocale(locale);
630     refreshProperties();
631     UIDesignerToolWindowManager.getInstance(getProject()).updateComponentTree();
632     DaemonCodeAnalyzer.getInstance(getProject()).restart();
633   }
634
635   @Nullable
636   public Locale getStringDescriptorLocale() {
637     return myRootContainer.getStringDescriptorLocale();
638   }
639
640   private void refreshProperties() {
641     final Ref<Boolean> anythingModified = new Ref<Boolean>();
642     FormEditingUtil.iterate(myRootContainer, new FormEditingUtil.ComponentVisitor() {
643       public boolean visit(final IComponent component) {
644         final RadComponent radComponent = (RadComponent)component;
645         boolean componentModified = false;
646         for(IProperty prop: component.getModifiedProperties()) {
647           if (prop instanceof IntroStringProperty) {
648             IntroStringProperty strProp = (IntroStringProperty) prop;
649             componentModified = strProp.refreshValue(radComponent) || componentModified;
650           }
651         }
652
653         if (component instanceof RadContainer) {
654           componentModified = ((RadContainer)component).updateBorder() || componentModified;
655         }
656
657         if (component.getParentContainer() instanceof RadTabbedPane) {
658           componentModified = ((RadTabbedPane) component.getParentContainer()).refreshChildTitle(radComponent) || componentModified;
659         }
660         if (componentModified) {
661           anythingModified.set(Boolean.TRUE);
662         }
663
664         return true;
665       }
666     });
667     if (!anythingModified.isNull()) {
668       refresh();
669       final UIDesignerToolWindowManager twm = UIDesignerToolWindowManager.getInstance(getProject());
670       twm.getComponentTree().repaint();
671       twm.getPropertyInspector().synchWithTree(true);
672     }
673   }
674
675   public MainProcessor getMainProcessor() {
676     return myProcessor;
677   }
678
679   public void refreshIntentionHint() {
680     myQuickFixManager.refreshIntentionHint();
681   }
682
683   public void setSelectionAnchor(final RadComponent component) {
684     mySelectionAnchor = new ComponentPtr(this, component);
685   }
686
687   @Nullable
688   public RadComponent getSelectionAnchor() {
689     if (mySelectionAnchor == null) return null;
690     mySelectionAnchor.validate();
691     return mySelectionAnchor.getComponent();
692   }
693
694   public void setSelectionLead(final RadComponent component) {
695     mySelectionLead = new ComponentPtr(this, component);
696   }
697
698   @Nullable
699   public RadComponent getSelectionLead() {
700     if (mySelectionLead == null) return null;
701     mySelectionLead.validate();
702     return mySelectionLead.getComponent();
703   }
704
705   public void scrollComponentInView(final RadComponent component) {
706     Rectangle rect = SwingUtilities.convertRectangle(component.getDelegee().getParent(), component.getBounds(), myLayeredPane);
707     myLayeredPane.scrollRectToVisible(rect);
708   }
709
710   public static final class ReplaceInfo {
711     private final int myStartOffset;
712     private final int myEndOffset;
713     private final String myReplacement;
714
715     public ReplaceInfo(final int startOffset, final int endOffset, final String replacement) {
716       myStartOffset = startOffset;
717       myEndOffset = endOffset;
718       myReplacement = replacement;
719     }
720
721     public int getStartOffset() {
722       return myStartOffset;
723     }
724
725     public int getEndOffset() {
726       return myEndOffset;
727     }
728
729     public String getReplacement() {
730       return myReplacement;
731     }
732   }
733
734   public static ReplaceInfo findFragmentToChange(final String oldText, final String newText) {
735     if (oldText.equals(newText)) {
736       return new ReplaceInfo(-1, -1, null);
737     }
738
739     final int oldLength = oldText.length();
740     final int newLength = newText.length();
741
742     int startOffset = 0;
743     while (
744       startOffset < oldLength && startOffset < newLength &&
745       oldText.charAt(startOffset) == newText.charAt(startOffset)
746       ) {
747       startOffset++;
748     }
749
750     int endOffset = oldLength;
751     while (true) {
752       if (endOffset <= startOffset) {
753         break;
754       }
755       final int idxInNew = newLength - (oldLength - endOffset) - 1;
756       if (idxInNew < startOffset) {
757         break;
758       }
759
760       final char c1 = oldText.charAt(endOffset - 1);
761       final char c2 = newText.charAt(idxInNew);
762       if (c1 != c2) {
763         break;
764       }
765       endOffset--;
766     }
767
768     return new ReplaceInfo(startOffset, endOffset, newText.substring(startOffset, newLength - (oldLength - endOffset)));
769   }
770
771   /**
772    * @param rootContainer new container to be set as a root.
773    */
774   private void setRootContainer(@NotNull final RadRootContainer rootContainer) {
775     if (myRootContainer != null) {
776       myLayeredPane.remove(myRootContainer.getDelegee());
777     }
778     myRootContainer = rootContainer;
779     setDesignTimeInsets(2);
780     myLayeredPane.add(myRootContainer.getDelegee(), LAYER_COMPONENT);
781
782     fireHierarchyChanged();
783   }
784
785   public void setDesignTimeInsets(final int insets) {
786     Integer oldInsets = (Integer) myRootContainer.getDelegee().getClientProperty(GridLayoutManager.DESIGN_TIME_INSETS);
787     if (oldInsets == null || oldInsets.intValue() != insets) {
788       myRootContainer.getDelegee().putClientProperty(GridLayoutManager.DESIGN_TIME_INSETS, insets);
789       revalidateRecursive(myRootContainer.getDelegee());
790     }
791   }
792
793   private static void revalidateRecursive(final JComponent component) {
794     for(Component child: component.getComponents()) {
795       if (child instanceof JComponent) {
796         revalidateRecursive((JComponent)child);
797       }
798     }
799     component.revalidate();
800     component.repaint();
801   }
802
803   /**
804    * Creates and sets new <code>RadRootContainer</code>
805    * @param keepSelection if true, the GUI designer tries to preserve the selection state after reload.
806    */
807   public void readFromFile(final boolean keepSelection) {
808     try {
809       ComponentPtr[] selection = null;
810       Map<String, String> tabbedPaneSelectedTabs = null;
811       if (keepSelection) {
812         selection = SelectionState.getSelection(this);
813         tabbedPaneSelectedTabs = saveTabbedPaneSelectedTabs();
814       }
815       Locale oldLocale = null;
816       if (myRootContainer != null) {
817         oldLocale = myRootContainer.getStringDescriptorLocale();
818       }
819
820       final String text = myDocument.getText();
821
822       final ClassLoader classLoader = LoaderFactory.getInstance(myModule.getProject()).getLoader(myFile);
823
824       final LwRootContainer rootContainer = Utils.getRootContainer(text, new CompiledClassPropertiesProvider(classLoader));
825       final RadRootContainer container = XmlReader.createRoot(myModule, rootContainer, classLoader, oldLocale);
826       setRootContainer(container);
827       if (keepSelection) {
828         SelectionState.restoreSelection(this, selection);
829         restoreTabbedPaneSelectedTabs(tabbedPaneSelectedTabs);
830       }
831       myInvalid = false;
832       myCardLayout.show(this, CARD_VALID);
833       refresh();
834     }
835     catch (Exception exc) {
836       Throwable original = exc;
837       while (original instanceof InvocationTargetException) {
838         original = original.getCause();
839       }
840       showInvalidCard(original);
841     }
842     catch(final LinkageError exc) {
843       showInvalidCard(exc);
844     }
845   }
846
847   private void showInvalidCard(final Throwable exc) {
848     LOG.info(exc);
849     // setting fictive container
850     setRootContainer(new RadRootContainer(myModule, "0"));
851     myFormInvalidLabel.setText(UIDesignerBundle.message("error.form.file.is.invalid.message", FormEditingUtil.getExceptionMessage(exc)));
852     myInvalid = true;
853     myCardLayout.show(this, CARD_INVALID);
854     repaint();
855   }
856
857   public boolean isFormInvalid() {
858     return myInvalid;
859   }
860
861   private Map<String, String> saveTabbedPaneSelectedTabs() {
862     final Map<String, String> result = new HashMap<String, String>();
863     FormEditingUtil.iterate(getRootContainer(), new FormEditingUtil.ComponentVisitor() {
864       public boolean visit(final IComponent component) {
865         if (component instanceof RadTabbedPane) {
866           RadTabbedPane tabbedPane = (RadTabbedPane) component;
867           RadComponent c = tabbedPane.getSelectedTab();
868           if (c != null) {
869             result.put(tabbedPane.getId(), c.getId());
870           }
871         }
872         return true;
873       }
874     });
875     return result;
876   }
877
878   private void restoreTabbedPaneSelectedTabs(final Map<String, String> tabbedPaneSelectedTabs) {
879     FormEditingUtil.iterate(getRootContainer(), new FormEditingUtil.ComponentVisitor() {
880       public boolean visit(final IComponent component) {
881         if (component instanceof RadTabbedPane) {
882           RadTabbedPane tabbedPane = (RadTabbedPane) component;
883           String selectedTabId = tabbedPaneSelectedTabs.get(tabbedPane.getId());
884           if (selectedTabId != null) {
885             for(RadComponent c: tabbedPane.getComponents()) {
886               if (c.getId().equals(selectedTabId)) {
887                 tabbedPane.selectTab(c);
888                 break;
889               }
890             }
891           }
892         }
893         return true;
894       }
895     });
896   }
897
898   public JComponent getPreferredFocusedComponent() {
899     if (myValidCard.isVisible()) {
900       return myGlassLayer;
901     }
902     else {
903       return myInvalidCard;
904     }
905   }
906
907   public static void repaintLayeredPane(final RadComponent component) {
908     final GuiEditor uiEditor = (GuiEditor)SwingUtilities.getAncestorOfClass(GuiEditor.class, component.getDelegee());
909     if (uiEditor != null) {
910       uiEditor.repaintLayeredPane();
911     }
912   }
913
914   public boolean isShowGrid() {
915     return myShowGrid;
916   }
917
918   public void setShowGrid(final boolean showGrid) {
919     if (myShowGrid != showGrid) {
920       myShowGrid = showGrid;
921       repaint();
922     }
923   }
924
925   public boolean isShowComponentTags() {
926     return myShowComponentTags;
927   }
928
929   public void setShowComponentTags(final boolean showComponentTags) {
930     if (myShowComponentTags != showComponentTags) {
931       myShowComponentTags = showComponentTags;
932       repaint();
933     }
934   }
935
936   public DesignDropTargetListener getDropTargetListener() {
937     return myDropTargetListener;
938   }
939
940   @Nullable
941   public GridCaptionPanel getFocusedCaptionPanel() {
942     if (myHorzCaptionPanel.isFocusOwner()) {
943       return myHorzCaptionPanel;
944     }
945     else if (myVertCaptionPanel.isFocusOwner()) {
946       return myVertCaptionPanel;
947     }
948     return null;
949   }
950
951   private boolean isActiveEditor() {
952     return UIDesignerToolWindowManager.getInstance(getProject()).getActiveFormEditor() == this;
953   }
954
955   void hideIntentionHint() {
956     myQuickFixManager.hideIntentionHint();
957   }
958
959   private final class MyLayeredPane extends JLayeredPane implements Scrollable {
960     /**
961      * All components allocate whole pane's area.
962      */
963     public void doLayout() {
964       for (int i = getComponentCount() - 1; i >= 0; i--) {
965         final Component component = getComponent(i);
966         component.setBounds(0, 0, getWidth(), getHeight());
967       }
968     }
969
970     public Dimension getMinimumSize() {
971       return getPreferredSize();
972     }
973
974     public Dimension getPreferredSize() {
975       // make sure all components fit
976       int width = 0;
977       int height = 0;
978       for (int i = 0; i < myRootContainer.getComponentCount(); i++) {
979         final RadComponent component = myRootContainer.getComponent(i);
980         width = Math.max(width, component.getX() + component.getWidth());
981         height = Math.max(height, component.getY() + component.getHeight());
982       }
983
984       width += 50;
985       height += 40;
986
987       return new Dimension(width, height);
988     }
989
990     public Dimension getPreferredScrollableViewportSize() {
991       return getPreferredSize();
992     }
993
994     public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
995       return 10;
996     }
997
998     public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
999       if (orientation == SwingConstants.HORIZONTAL) {
1000         return visibleRect.width-10;
1001       }
1002       return visibleRect.height-10;
1003     }
1004
1005     public boolean getScrollableTracksViewportWidth() {
1006       return false;
1007     }
1008
1009     public boolean getScrollableTracksViewportHeight() {
1010       return false;
1011     }
1012   }
1013
1014   /**
1015    * Action works only if we are not editing something in the property inspector
1016    */
1017   private final class CancelCurrentOperationAction extends AnAction {
1018     public void actionPerformed(final AnActionEvent e) {
1019       myProcessor.cancelOperation();
1020       myQuickFixManager.hideIntentionHint();
1021     }
1022
1023     public void update(final AnActionEvent e) {
1024       final UIDesignerToolWindowManager manager = UIDesignerToolWindowManager.getInstance(getProject());
1025       e.getPresentation().setEnabled(!manager.getPropertyInspector().isEditing());
1026     }
1027   }
1028
1029   /**
1030    * Allows "DEL" button to work through the standard mechanism
1031    */
1032   private final class MyDeleteProvider implements DeleteProvider {
1033     public void deleteElement(final DataContext dataContext) {
1034       if (!GuiEditor.this.ensureEditable()) {
1035         return;
1036       }
1037       CommandProcessor.getInstance().executeCommand(getProject(), new Runnable() {
1038         public void run() {
1039           FormEditingUtil.deleteSelection(GuiEditor.this);
1040         }
1041       }, UIDesignerBundle.message("command.delete.selection"), null);
1042     }
1043
1044     public boolean canDeleteElement(final DataContext dataContext) {
1045       final UIDesignerToolWindowManager manager = UIDesignerToolWindowManager.getInstance(getProject());
1046       return
1047         !manager.getPropertyInspector().isEditing() &&
1048         !myInplaceEditingLayer.isEditing() &&
1049         FormEditingUtil.canDeleteSelection(GuiEditor.this);
1050     }
1051   }
1052
1053   /**
1054    * Listens PSI event and update error highlighting in the UI editor
1055    */
1056   private final class MyPsiTreeChangeListener extends PsiTreeChangeAdapter {
1057     private final Alarm myAlarm;
1058     private final MyRefreshPropertiesRequest myRefreshPropertiesRequest = new MyRefreshPropertiesRequest();
1059     private final MySynchronizeRequest mySynchronizeRequest = new MySynchronizeRequest(myModule, true);
1060
1061     public MyPsiTreeChangeListener() {
1062       myAlarm = new Alarm();
1063     }
1064
1065     /**
1066      * Cancels all pending update requests. You have to cancel all pending requests
1067      * to not access to closed project.
1068      */
1069     public void dispose() {
1070       myAlarm.cancelAllRequests();
1071     }
1072
1073     public void childAdded(final PsiTreeChangeEvent event) {
1074       handleEvent(event);
1075     }
1076
1077     public void childMoved(final PsiTreeChangeEvent event) {
1078       handleEvent(event);
1079     }
1080
1081     public void childrenChanged(final PsiTreeChangeEvent event) {
1082       handleEvent(event);
1083     }
1084
1085     public void childRemoved(PsiTreeChangeEvent event) {
1086       handleEvent(event);
1087     }
1088
1089     public void childReplaced(PsiTreeChangeEvent event) {
1090       handleEvent(event);
1091     }
1092
1093     public void propertyChanged(final PsiTreeChangeEvent event) {
1094       if (PsiTreeChangeEvent.PROP_ROOTS.equals(event.getPropertyName())) {
1095         myAlarm.cancelRequest(myRefreshPropertiesRequest);
1096         myAlarm.addRequest(myRefreshPropertiesRequest, 500, ModalityState.stateForComponent(GuiEditor.this));
1097       }
1098     }
1099
1100     private void handleEvent(final PsiTreeChangeEvent event) {
1101       if (event.getParent() != null) {
1102         PsiFile containingFile = event.getParent().getContainingFile();
1103         if (containingFile instanceof PropertiesFile) {
1104           LOG.debug("Received PSI change event for properties file");
1105           myAlarm.cancelRequest(myRefreshPropertiesRequest);
1106           myAlarm.addRequest(myRefreshPropertiesRequest, 500, ModalityState.stateForComponent(GuiEditor.this));
1107         }
1108         else if (containingFile instanceof PsiPlainTextFile && containingFile.getFileType().equals(StdFileTypes.GUI_DESIGNER_FORM)) {
1109           // quick check if relevant
1110           String resourceName = FormEditingUtil.buildResourceName(containingFile);
1111           if (myDocument.getText().indexOf(resourceName) >= 0) {
1112             LOG.debug("Received PSI change event for nested form");
1113             // TODO[yole]: handle multiple nesting
1114             myAlarm.cancelRequest(mySynchronizeRequest);
1115             myAlarm.addRequest(mySynchronizeRequest, 500, ModalityState.stateForComponent(GuiEditor.this));
1116           }
1117         }
1118       }
1119     }
1120   }
1121
1122   private class MySynchronizeRequest implements Runnable {
1123     private final Module myModule;
1124     private final boolean myKeepSelection;
1125
1126     public MySynchronizeRequest(final Module module, final boolean keepSelection) {
1127       myModule = module;
1128       myKeepSelection = keepSelection;
1129     }
1130
1131     public void run() {
1132       LOG.debug("Synchronizing GUI editor " + myFile.getName() + " to document");
1133       PsiDocumentManager.getInstance(myModule.getProject()).commitDocument(myDocument);
1134       readFromFile(myKeepSelection);
1135     }
1136   }
1137
1138   private class MyRefreshPropertiesRequest implements Runnable {
1139     public void run() {
1140       if (!myModule.isDisposed() && !getProject().isDisposed()) {
1141         refreshProperties();
1142       }
1143     }
1144   }
1145
1146   private class MyPaletteKeyListener extends KeyAdapter {
1147     @Override public void keyPressed(KeyEvent e) {
1148       PaletteManager paletteManager = PaletteManager.getInstance(getProject());
1149       if (e.getKeyCode() == KeyEvent.VK_SHIFT && paletteManager.getActiveItem(ComponentItem.class) != null && isActiveEditor()) {
1150         setDesignTimeInsets(12);
1151       }
1152     }
1153
1154     @Override public void keyReleased(KeyEvent e) {
1155       if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
1156         setDesignTimeInsets(2);
1157       }
1158     }
1159   }
1160
1161   private class MyPaletteDragListener implements PaletteDragEventListener {
1162     public void dropActionChanged(int gestureModifiers) {
1163       if ((gestureModifiers & InputEvent.SHIFT_MASK) != 0 && isActiveEditor()) {
1164         setDesignTimeInsets(12);
1165       }
1166       else {
1167         setDesignTimeInsets(2);
1168       }
1169     }
1170   }
1171
1172   private class MyPaletteSelectionListener implements ListSelectionListener {
1173     public void valueChanged(ListSelectionEvent e) {
1174       if (PaletteManager.getInstance(getProject()).getActiveItem() == null) {
1175         myProcessor.cancelPaletteInsert();
1176       }
1177     }
1178   }
1179 }