| /* |
| * Copyright 2000-2014 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.codeInsight.daemon.DaemonCodeAnalyzer; |
| import com.intellij.designer.DesignerEditorPanelFacade; |
| import com.intellij.designer.LightFillLayout; |
| import com.intellij.ide.DeleteProvider; |
| import com.intellij.ide.highlighter.XmlFileHighlighter; |
| import com.intellij.ide.palette.impl.PaletteToolWindowManager; |
| import com.intellij.lang.properties.psi.PropertiesFile; |
| import com.intellij.openapi.actionSystem.*; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.application.ModalityState; |
| import com.intellij.openapi.command.CommandProcessor; |
| import com.intellij.openapi.command.undo.UndoManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.editor.Editor; |
| import com.intellij.openapi.editor.EditorFactory; |
| import com.intellij.openapi.editor.colors.EditorColorsManager; |
| import com.intellij.openapi.editor.event.DocumentAdapter; |
| import com.intellij.openapi.editor.event.DocumentEvent; |
| import com.intellij.openapi.editor.ex.EditorEx; |
| import com.intellij.openapi.editor.ex.util.LexerEditorHighlighter; |
| import com.intellij.openapi.fileEditor.FileDocumentManager; |
| import com.intellij.openapi.fileTypes.StdFileTypes; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.module.ModuleUtil; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.ui.DialogBuilder; |
| import com.intellij.openapi.ui.ThreeComponentsSplitter; |
| import com.intellij.openapi.util.Disposer; |
| import com.intellij.openapi.util.Ref; |
| import com.intellij.openapi.util.registry.Registry; |
| import com.intellij.openapi.vfs.ReadonlyStatusHandler; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.*; |
| import com.intellij.ui.JBColor; |
| import com.intellij.ui.ScrollPaneFactory; |
| import com.intellij.ui.components.JBLayeredPane; |
| import com.intellij.uiDesigner.*; |
| import com.intellij.uiDesigner.compiler.Utils; |
| import com.intellij.uiDesigner.componentTree.ComponentPtr; |
| import com.intellij.uiDesigner.componentTree.ComponentSelectionListener; |
| import com.intellij.uiDesigner.componentTree.ComponentTree; |
| import com.intellij.uiDesigner.core.GridLayoutManager; |
| import com.intellij.uiDesigner.core.Util; |
| import com.intellij.uiDesigner.editor.UIFormEditor; |
| import com.intellij.uiDesigner.lw.CompiledClassPropertiesProvider; |
| import com.intellij.uiDesigner.lw.IComponent; |
| import com.intellij.uiDesigner.lw.IProperty; |
| import com.intellij.uiDesigner.lw.LwRootContainer; |
| import com.intellij.uiDesigner.palette.ComponentItem; |
| import com.intellij.uiDesigner.propertyInspector.DesignerToolWindow; |
| import com.intellij.uiDesigner.propertyInspector.DesignerToolWindowManager; |
| import com.intellij.uiDesigner.propertyInspector.PropertyInspector; |
| import com.intellij.uiDesigner.propertyInspector.properties.IntroStringProperty; |
| import com.intellij.uiDesigner.radComponents.RadComponent; |
| import com.intellij.uiDesigner.radComponents.RadContainer; |
| import com.intellij.uiDesigner.radComponents.RadRootContainer; |
| import com.intellij.uiDesigner.radComponents.RadTabbedPane; |
| import com.intellij.util.Alarm; |
| import com.intellij.util.NotNullProducer; |
| import com.intellij.util.ui.UIUtil; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import javax.swing.event.EventListenerList; |
| import javax.swing.event.ListSelectionEvent; |
| import java.awt.*; |
| import java.awt.dnd.DnDConstants; |
| import java.awt.dnd.DropTarget; |
| import java.awt.event.FocusEvent; |
| import java.awt.event.FocusListener; |
| import java.awt.event.InputEvent; |
| import java.awt.event.KeyEvent; |
| import java.lang.reflect.InvocationTargetException; |
| import java.util.HashMap; |
| import java.util.Locale; |
| import java.util.Map; |
| |
| /** |
| * <code>GuiEditor</code> is a panel with border layout. It has palette at the north, |
| * tree of component with property editor at the west and editor area at the center. |
| * This editor area contains internal component where user edit the UI. |
| * |
| * @author Anton Katilin |
| * @author Vladimir Kondratyev |
| */ |
| public final class GuiEditor extends JPanel implements DesignerEditorPanelFacade, DataProvider, ModuleProvider { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.uiDesigner.GuiEditor"); |
| |
| private final Project myProject; |
| @NotNull private final UIFormEditor myEditor; |
| private Module myModule; |
| @NotNull private final VirtualFile myFile; |
| |
| /** |
| * for debug purposes |
| */ |
| private Exception myWhere; |
| |
| /** |
| * All component are on this layer |
| */ |
| private static final Integer LAYER_COMPONENT = JLayeredPane.DEFAULT_LAYER; |
| /** |
| * This layer contains all "passive" decorators such as component boundaries |
| * and selection rectangle. |
| */ |
| private static final Integer LAYER_PASSIVE_DECORATION = JLayeredPane.POPUP_LAYER; |
| /** |
| * We show (and move) dragged component at this layer |
| */ |
| private static final Integer LAYER_DND = JLayeredPane.DRAG_LAYER; |
| /** |
| * This is the topmost layer. It gets and redispatch all incoming events |
| */ |
| private static final Integer LAYER_GLASS = new Integer(JLayeredPane.DRAG_LAYER.intValue() + 100); |
| /** |
| * This layer contains all "active" decorators. This layer should be over |
| * LAYER_GLASS because active decorators must get AWT events to work correctly. |
| */ |
| private static final Integer LAYER_ACTIVE_DECORATION = new Integer(LAYER_GLASS.intValue() + 100); |
| /** |
| * This layer contains all inplace editors. |
| */ |
| private static final Integer LAYER_INPLACE_EDITING = new Integer(LAYER_ACTIVE_DECORATION.intValue() + 100); |
| |
| private final EventListenerList myListenerList; |
| /** |
| * we have to store document here but not file because there can be a situation when |
| * document we added listener to has been disposed, and remove listener will be applied to |
| * a new document (got by file) -> assertion (see SCR 14143) |
| */ |
| private final Document myDocument; |
| |
| final MainProcessor myProcessor; |
| @NotNull private final JScrollPane myScrollPane; |
| /** |
| * This layered pane contains all layers to lay components out and to |
| * show all necessary decoration items |
| */ |
| @NotNull private final MyLayeredPane myLayeredPane; |
| /** |
| * The component which represents decoration layer. All passive |
| * decorators are on this layer. |
| */ |
| private final PassiveDecorationLayer myDecorationLayer; |
| /** |
| * The component which represents layer where located all dragged |
| * components |
| */ |
| private final DragLayer myDragLayer; |
| /** |
| * This layer contains all inplace editors |
| */ |
| private final InplaceEditingLayer myInplaceEditingLayer; |
| /** |
| * Brings functionality to "DEL" button |
| */ |
| private final MyDeleteProvider myDeleteProvider; |
| /** |
| * Rerun error analizer |
| */ |
| private final MyPsiTreeChangeListener myPsiTreeChangeListener; |
| |
| private RadRootContainer myRootContainer; |
| /** |
| * Panel with components palette. |
| */ |
| //@NotNull private final PalettePanel myPalettePanel; |
| /** |
| * GuiEditor should not react on own events. If <code>myInsideChange</code> |
| * is <code>true</code> then we do not react on incoming DocumentEvent. |
| */ |
| private boolean myInsideChange; |
| private final DocumentAdapter myDocumentListener; |
| private final CardLayout myCardLayout = new CardLayout(); |
| private final ThreeComponentsSplitter myContentSplitter = new ThreeComponentsSplitter(); |
| private final JPanel myCardPanel = new JPanel(myCardLayout); |
| |
| @NonNls private final static String CARD_VALID = "valid"; |
| @NonNls private final static String CARD_INVALID = "invalid"; |
| private final JPanel myValidCard; |
| private final JPanel myInvalidCard; |
| private boolean myInvalid = false; |
| |
| private final CutCopyPasteSupport myCutCopyPasteSupport; |
| /** |
| * Implementation of Crtl+W and Ctrl+Shift+W behavior |
| */ |
| private final SelectionState mySelectionState; |
| @NotNull private final GlassLayer myGlassLayer; |
| private final ActiveDecorationLayer myActiveDecorationLayer; |
| |
| private boolean myShowGrid = true; |
| private boolean myShowComponentTags = true; |
| private final DesignDropTargetListener myDropTargetListener; |
| private JLabel myFormInvalidLabel; |
| private final QuickFixManagerImpl myQuickFixManager; |
| private final GridCaptionPanel myHorzCaptionPanel; |
| private final GridCaptionPanel myVertCaptionPanel; |
| private ComponentPtr mySelectionAnchor; |
| private ComponentPtr mySelectionLead; |
| /** |
| * Undo group ID for undoing actions that need to be undone together with the form modification. |
| */ |
| private Object myNextSaveGroupId = new Object(); |
| |
| @NonNls private static final String ourHelpID = "guiDesigner.uiTour.workspace"; |
| |
| public static final DataKey<GuiEditor> DATA_KEY = DataKey.create(GuiEditor.class.getName()); |
| |
| /** |
| * @param file file to be edited |
| * @throws java.lang.IllegalArgumentException if the <code>file</code> |
| * is <code>null</code> or <code>file</code> is not valid PsiFile |
| */ |
| public GuiEditor(@NotNull UIFormEditor editor, @NotNull Project project, @NotNull Module module, @NotNull VirtualFile file) { |
| myEditor = editor; |
| LOG.assertTrue(file.isValid()); |
| |
| myProject = project; |
| myModule = module; |
| myFile = file; |
| |
| myCutCopyPasteSupport = new CutCopyPasteSupport(this); |
| |
| setLayout(new BorderLayout()); |
| |
| myContentSplitter.setDividerWidth(0); |
| myContentSplitter.setDividerMouseZoneSize(Registry.intValue("ide.splitter.mouseZone")); |
| add(myContentSplitter, BorderLayout.CENTER); |
| |
| myValidCard = new JPanel(new BorderLayout()); |
| myInvalidCard = createInvalidCard(); |
| |
| myCardPanel.add(myValidCard, CARD_VALID); |
| myCardPanel.add(myInvalidCard, CARD_INVALID); |
| |
| JPanel contentPanel = new JPanel(new LightFillLayout()); |
| JLabel toolbar = new JLabel(); |
| toolbar.setVisible(false); |
| contentPanel.add(toolbar); |
| contentPanel.add(myCardPanel); |
| |
| myContentSplitter.setInnerComponent(contentPanel); |
| |
| myListenerList = new EventListenerList(); |
| |
| myDecorationLayer = new PassiveDecorationLayer(this); |
| myDragLayer = new DragLayer(this); |
| |
| myLayeredPane = new MyLayeredPane(); |
| myInplaceEditingLayer = new InplaceEditingLayer(this); |
| myLayeredPane.add(myInplaceEditingLayer, LAYER_INPLACE_EDITING); |
| myActiveDecorationLayer = new ActiveDecorationLayer(this); |
| myLayeredPane.add(myActiveDecorationLayer, LAYER_ACTIVE_DECORATION); |
| myGlassLayer = new GlassLayer(this); |
| myLayeredPane.add(myGlassLayer, LAYER_GLASS); |
| myLayeredPane.add(myDecorationLayer, LAYER_PASSIVE_DECORATION); |
| myLayeredPane.add(myDragLayer, LAYER_DND); |
| |
| myGlassLayer.addFocusListener(new FocusListener() { |
| public void focusGained(FocusEvent e) { |
| myDecorationLayer.repaint(); |
| //fireSelectedComponentChanged(); // EA-36478 |
| } |
| |
| public void focusLost(FocusEvent e) { |
| myDecorationLayer.repaint(); |
| } |
| }); |
| |
| // Ctrl+W / Ctrl+Shift+W support |
| mySelectionState = new SelectionState(this); |
| |
| // DeleteProvider |
| myDeleteProvider = new MyDeleteProvider(); |
| |
| // We need to synchronize GUI editor with the document |
| final Alarm alarm = new Alarm(); |
| myDocumentListener = new DocumentAdapter() { |
| public void documentChanged(final DocumentEvent e) { |
| if (!myInsideChange) { |
| UndoManager undoManager = UndoManager.getInstance(getProject()); |
| alarm.cancelAllRequests(); |
| alarm.addRequest(new MySynchronizeRequest(undoManager.isUndoInProgress() || undoManager.isRedoInProgress()), |
| 100/*any arbitrary delay*/, ModalityState.stateForComponent(GuiEditor.this)); |
| } |
| } |
| }; |
| |
| // Prepare document |
| myDocument = FileDocumentManager.getInstance().getDocument(file); |
| myDocument.addDocumentListener(myDocumentListener); |
| |
| // Read form from file |
| readFromFile(false); |
| |
| JPanel panel = new JPanel(new GridBagLayout()); |
| panel.setBackground(GridCaptionPanel.getGutterColor()); |
| |
| myHorzCaptionPanel = new GridCaptionPanel(this, false); |
| myVertCaptionPanel = new GridCaptionPanel(this, true); |
| |
| GridBagConstraints gbc = new GridBagConstraints(); |
| gbc.gridx = 0; |
| gbc.gridy = 1; |
| gbc.weightx = 0.0; |
| gbc.weighty = 0.0; |
| gbc.fill = GridBagConstraints.BOTH; |
| panel.add(myVertCaptionPanel, gbc); |
| |
| gbc.gridx = 1; |
| gbc.gridy = 0; |
| panel.add(myHorzCaptionPanel, gbc); |
| |
| gbc.gridx = 1; |
| gbc.gridy = 1; |
| gbc.weightx = 1.0; |
| gbc.weighty = 1.0; |
| |
| myScrollPane = ScrollPaneFactory.createScrollPane(myLayeredPane); |
| myScrollPane.setBackground(new JBColor(new NotNullProducer<Color>() { |
| @NotNull |
| @Override |
| public Color produce() { |
| return EditorColorsManager.getInstance().getGlobalScheme().getDefaultBackground(); |
| } |
| })); |
| panel.add(myScrollPane, gbc); |
| myHorzCaptionPanel.attachToScrollPane(myScrollPane); |
| myVertCaptionPanel.attachToScrollPane(myScrollPane); |
| |
| myValidCard.add(panel, BorderLayout.CENTER); |
| |
| final CancelCurrentOperationAction cancelCurrentOperationAction = new CancelCurrentOperationAction(); |
| cancelCurrentOperationAction.registerCustomShortcutSet(CommonShortcuts.ESCAPE, this); |
| |
| myProcessor = new MainProcessor(this); |
| |
| // PSI listener to restart error highlighter |
| myPsiTreeChangeListener = new MyPsiTreeChangeListener(); |
| PsiManager.getInstance(getProject()).addPsiTreeChangeListener(myPsiTreeChangeListener); |
| |
| myQuickFixManager = new QuickFixManagerImpl(this, myGlassLayer, myScrollPane.getViewport()); |
| |
| myDropTargetListener = new DesignDropTargetListener(this); |
| if (!ApplicationManager.getApplication().isHeadlessEnvironment()) { |
| new DropTarget(getGlassLayer(), DnDConstants.ACTION_COPY_OR_MOVE, myDropTargetListener); |
| } |
| |
| myActiveDecorationLayer.installSelectionWatcher(); |
| |
| ActionManager.getInstance().getAction("GuiDesigner.IncreaseIndent").registerCustomShortcutSet( |
| new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0)), myGlassLayer); |
| ActionManager.getInstance().getAction("GuiDesigner.DecreaseIndent").registerCustomShortcutSet( |
| new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, KeyEvent.SHIFT_MASK)), myGlassLayer); |
| |
| UIUtil.invokeLaterIfNeeded(new Runnable() { |
| @Override |
| public void run() { |
| DesignerToolWindowManager.getInstance(myProject).bind(GuiEditor.this); |
| PaletteToolWindowManager.getInstance(myProject).bind(GuiEditor.this); |
| } |
| }); |
| } |
| |
| @Override |
| public ThreeComponentsSplitter getContentSplitter() { |
| return myContentSplitter; |
| } |
| |
| @NotNull |
| public UIFormEditor getEditor() { |
| return myEditor; |
| } |
| |
| @NotNull |
| public SelectionState getSelectionState() { |
| return mySelectionState; |
| } |
| |
| public void dispose() { |
| ApplicationManager.getApplication().assertIsDispatchThread(); |
| |
| if (myWhere != null) { |
| LOG.error("Already disposed: old trace: ", myWhere); |
| LOG.error("Already disposed: new trace: "); |
| } |
| else { |
| myWhere = new Exception(); |
| } |
| |
| myDocument.removeDocumentListener(myDocumentListener); |
| PsiManager.getInstance(getProject()).removePsiTreeChangeListener(myPsiTreeChangeListener); |
| |
| DesignerToolWindowManager.getInstance(myProject).dispose(this); |
| PaletteToolWindowManager.getInstance(myProject).dispose(this); |
| myPsiTreeChangeListener.dispose(); |
| |
| Disposer.dispose(myContentSplitter); |
| } |
| |
| @NotNull |
| @Override |
| public Module getModule() { |
| if (myModule.isDisposed()) { |
| myModule = ModuleUtil.findModuleForFile(myFile, myProject); |
| if (myModule == null) { |
| throw new IllegalArgumentException("No module for file " + myFile + " in project " + myModule); |
| } |
| } |
| return myModule; |
| } |
| |
| @NotNull |
| @Override |
| public Project getProject() { |
| return myProject; |
| } |
| |
| @NotNull |
| public VirtualFile getFile() { |
| return myFile; |
| } |
| |
| public PsiFile getPsiFile() { |
| return PsiManager.getInstance(getProject()).findFile(myFile); |
| } |
| |
| public boolean isEditable() { |
| final Document document = FileDocumentManager.getInstance().getDocument(myFile); |
| return document != null && document.isWritable(); |
| } |
| |
| public boolean ensureEditable() { |
| if (isEditable()) { |
| return true; |
| } |
| VirtualFile sourceFileToCheckOut = null; |
| if (!GuiDesignerConfiguration.getInstance(getProject()).INSTRUMENT_CLASSES) { |
| final String classToBind = myRootContainer.getClassToBind(); |
| if (classToBind != null && classToBind.length() > 0) { |
| PsiClass psiClass = FormEditingUtil.findClassToBind(getModule(), classToBind); |
| if (psiClass != null) { |
| sourceFileToCheckOut = psiClass.getContainingFile().getVirtualFile(); |
| } |
| } |
| } |
| |
| final ReadonlyStatusHandler.OperationStatus status; |
| if (sourceFileToCheckOut != null) { |
| status = ReadonlyStatusHandler.getInstance(getProject()).ensureFilesWritable(myFile, sourceFileToCheckOut); |
| } |
| else { |
| status = ReadonlyStatusHandler.getInstance(getProject()).ensureFilesWritable(myFile); |
| } |
| return !status.hasReadonlyFiles(); |
| } |
| |
| public void refresh() { |
| refreshImpl(myRootContainer); |
| myRootContainer.getDelegee().revalidate(); |
| repaintLayeredPane(); |
| } |
| |
| public void refreshAndSave(final boolean forceSync) { |
| // Update property inspector |
| final PropertyInspector propertyInspector = DesignerToolWindowManager.getInstance(this).getPropertyInspector(); |
| if (propertyInspector != null) { |
| propertyInspector.synchWithTree(forceSync); |
| } |
| |
| refresh(); |
| saveToFile(); |
| // TODO[yole]: install appropriate listeners so that the captions repaint themselves at correct time |
| myHorzCaptionPanel.repaint(); |
| myVertCaptionPanel.repaint(); |
| } |
| |
| public Object getNextSaveGroupId() { |
| return myNextSaveGroupId; |
| } |
| |
| private static void refreshImpl(final RadComponent component) { |
| if (component.getParent() != null) { |
| final Dimension size = component.getSize(); |
| final int oldWidth = size.width; |
| final int oldHeight = size.height; |
| Util.adjustSize(component.getDelegee(), component.getConstraints(), size); |
| |
| if (oldWidth != size.width || oldHeight != size.height) { |
| if (component.getParent().isXY()) { |
| component.setSize(size); |
| } |
| component.getDelegee().invalidate(); |
| } |
| } |
| |
| if (component instanceof RadContainer) { |
| component.refresh(); |
| |
| final RadContainer container = (RadContainer)component; |
| for (int i = container.getComponentCount() - 1; i >= 0; i--) { |
| refreshImpl(container.getComponent(i)); |
| } |
| } |
| } |
| |
| public Object getData(final String dataId) { |
| if (PlatformDataKeys.HELP_ID.is(dataId)) { |
| return ourHelpID; |
| } |
| |
| // Standard Swing cut/copy/paste actions should work if user is editing something inside property inspector |
| Project project = getProject(); |
| if (project.isDisposed()) return null; |
| final PropertyInspector inspector = DesignerToolWindowManager.getInstance(this).getPropertyInspector(); |
| if (inspector != null && inspector.isEditing()) { |
| return null; |
| } |
| |
| if (PlatformDataKeys.DELETE_ELEMENT_PROVIDER.is(dataId)) { |
| return myDeleteProvider; |
| } |
| |
| if (PlatformDataKeys.COPY_PROVIDER.is(dataId) || |
| PlatformDataKeys.CUT_PROVIDER.is(dataId) || |
| PlatformDataKeys.PASTE_PROVIDER.is(dataId)) { |
| return myCutCopyPasteSupport; |
| } |
| |
| return null; |
| } |
| |
| private JPanel createInvalidCard() { |
| final JPanel panel = new JPanel(new GridBagLayout()); |
| myFormInvalidLabel = new JLabel(UIDesignerBundle.message("error.form.file.is.invalid")); |
| panel.add(myFormInvalidLabel, |
| new GridBagConstraints(0, 0, 1, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0)); |
| return panel; |
| } |
| |
| /** |
| * @return the component which represents DnD layer. All currently |
| * dragged (moved) component are on this layer. |
| */ |
| public DragLayer getDragLayer() { |
| return myDragLayer; |
| } |
| |
| /** |
| * @return the topmost <code>UiConainer</code> which in the root of |
| * component hierarchy. This method never returns <code>null</code>. |
| */ |
| @NotNull |
| public RadRootContainer getRootContainer() { |
| return myRootContainer; |
| } |
| |
| /** |
| * Fires event that selection changes |
| */ |
| public void fireSelectedComponentChanged() { |
| final ComponentSelectionListener[] listeners = myListenerList.getListeners(ComponentSelectionListener.class); |
| for (ComponentSelectionListener listener : listeners) { |
| listener.selectedComponentChanged(this); |
| } |
| } |
| |
| private void fireHierarchyChanged() { |
| final HierarchyChangeListener[] listeners = myListenerList.getListeners(HierarchyChangeListener.class); |
| for (final HierarchyChangeListener listener : listeners) { |
| listener.hierarchyChanged(); |
| } |
| } |
| |
| @NotNull |
| public GlassLayer getGlassLayer() { |
| return myGlassLayer; |
| } |
| |
| /** |
| * @return the component which represents layer with active decorators |
| * such as grid edit controls, inplace editors, etc. |
| */ |
| public InplaceEditingLayer getInplaceEditingLayer() { |
| return myInplaceEditingLayer; |
| } |
| |
| @NotNull |
| public JLayeredPane getLayeredPane() { |
| return myLayeredPane; |
| } |
| |
| public void repaintLayeredPane() { |
| myLayeredPane.repaint(); |
| } |
| |
| /** |
| * Adds specified selection listener. This listener gets notification each time |
| * the selection in the component the changes. |
| */ |
| public void addComponentSelectionListener(final ComponentSelectionListener l) { |
| myListenerList.add(ComponentSelectionListener.class, l); |
| } |
| |
| /** |
| * Removes specified selection listener |
| */ |
| public void removeComponentSelectionListener(final ComponentSelectionListener l) { |
| myListenerList.remove(ComponentSelectionListener.class, l); |
| } |
| |
| /** |
| * Adds specified hierarchy change listener |
| */ |
| public void addHierarchyChangeListener(@NotNull final HierarchyChangeListener l) { |
| myListenerList.add(HierarchyChangeListener.class, l); |
| } |
| |
| /** |
| * Removes specified hierarchy change listener |
| */ |
| public void removeHierarchyChangeListener(@NotNull final HierarchyChangeListener l) { |
| myListenerList.remove(HierarchyChangeListener.class, l); |
| } |
| |
| private void saveToFile() { |
| LOG.debug("GuiEditor.saveToFile(): group ID=" + myNextSaveGroupId); |
| CommandProcessor.getInstance().executeCommand(getProject(), new Runnable() { |
| public void run() { |
| ApplicationManager.getApplication().runWriteAction(new Runnable() { |
| public void run() { |
| myInsideChange = true; |
| try { |
| final XmlWriter writer = new XmlWriter(); |
| getRootContainer().write(writer); |
| final String newText = writer.getText(); |
| final String oldText = myDocument.getText(); |
| |
| try { |
| final ReplaceInfo replaceInfo = findFragmentToChange(oldText, newText); |
| if (replaceInfo.getStartOffset() == -1) { |
| // do nothing - texts are equal |
| } |
| else { |
| myDocument.replaceString(replaceInfo.getStartOffset(), replaceInfo.getEndOffset(), replaceInfo.getReplacement()); |
| } |
| } |
| catch (Exception e) { |
| LOG.error(e); |
| myDocument.replaceString(0, oldText.length(), newText); |
| } |
| } |
| finally { |
| myInsideChange = false; |
| } |
| } |
| }); |
| } |
| }, "UI Designer Save", myNextSaveGroupId); |
| myNextSaveGroupId = new Object(); |
| |
| fireHierarchyChanged(); |
| } |
| |
| public ActiveDecorationLayer getActiveDecorationLayer() { |
| return myActiveDecorationLayer; |
| } |
| |
| public void setStringDescriptorLocale(final Locale locale) { |
| myRootContainer.setStringDescriptorLocale(locale); |
| refreshProperties(); |
| DesignerToolWindowManager.getInstance(this).updateComponentTree(); |
| DaemonCodeAnalyzer.getInstance(getProject()).restart(); |
| } |
| |
| @Nullable |
| public Locale getStringDescriptorLocale() { |
| return myRootContainer.getStringDescriptorLocale(); |
| } |
| |
| private void refreshProperties() { |
| final Ref<Boolean> anythingModified = new Ref<Boolean>(); |
| FormEditingUtil.iterate(myRootContainer, new FormEditingUtil.ComponentVisitor() { |
| public boolean visit(final IComponent component) { |
| final RadComponent radComponent = (RadComponent)component; |
| boolean componentModified = false; |
| for (IProperty prop : component.getModifiedProperties()) { |
| if (prop instanceof IntroStringProperty) { |
| IntroStringProperty strProp = (IntroStringProperty)prop; |
| componentModified = strProp.refreshValue(radComponent) || componentModified; |
| } |
| } |
| |
| if (component instanceof RadContainer) { |
| componentModified = ((RadContainer)component).updateBorder() || componentModified; |
| } |
| |
| if (component.getParentContainer() instanceof RadTabbedPane) { |
| componentModified = ((RadTabbedPane)component.getParentContainer()).refreshChildTitle(radComponent) || componentModified; |
| } |
| if (componentModified) { |
| anythingModified.set(Boolean.TRUE); |
| } |
| |
| return true; |
| } |
| }); |
| if (!anythingModified.isNull()) { |
| refresh(); |
| DesignerToolWindow designerToolWindow = DesignerToolWindowManager.getInstance(this); |
| ComponentTree tree = designerToolWindow.getComponentTree(); |
| if (tree != null) tree.repaint(); |
| PropertyInspector inspector = designerToolWindow.getPropertyInspector(); |
| if (inspector != null) inspector.synchWithTree(true); |
| } |
| } |
| |
| public MainProcessor getMainProcessor() { |
| return myProcessor; |
| } |
| |
| public void refreshIntentionHint() { |
| myQuickFixManager.refreshIntentionHint(); |
| } |
| |
| public void setSelectionAnchor(final RadComponent component) { |
| mySelectionAnchor = new ComponentPtr(this, component); |
| } |
| |
| @Nullable |
| public RadComponent getSelectionAnchor() { |
| if (mySelectionAnchor == null) return null; |
| mySelectionAnchor.validate(); |
| return mySelectionAnchor.getComponent(); |
| } |
| |
| public void setSelectionLead(final RadComponent component) { |
| mySelectionLead = new ComponentPtr(this, component); |
| } |
| |
| @Nullable |
| public RadComponent getSelectionLead() { |
| if (mySelectionLead == null) return null; |
| mySelectionLead.validate(); |
| return mySelectionLead.getComponent(); |
| } |
| |
| public void scrollComponentInView(final RadComponent component) { |
| Rectangle rect = SwingUtilities.convertRectangle(component.getDelegee().getParent(), component.getBounds(), myLayeredPane); |
| myLayeredPane.scrollRectToVisible(rect); |
| } |
| |
| public static final class ReplaceInfo { |
| private final int myStartOffset; |
| private final int myEndOffset; |
| private final String myReplacement; |
| |
| public ReplaceInfo(final int startOffset, final int endOffset, final String replacement) { |
| myStartOffset = startOffset; |
| myEndOffset = endOffset; |
| myReplacement = replacement; |
| } |
| |
| public int getStartOffset() { |
| return myStartOffset; |
| } |
| |
| public int getEndOffset() { |
| return myEndOffset; |
| } |
| |
| public String getReplacement() { |
| return myReplacement; |
| } |
| } |
| |
| public static ReplaceInfo findFragmentToChange(final String oldText, final String newText) { |
| if (oldText.equals(newText)) { |
| return new ReplaceInfo(-1, -1, null); |
| } |
| |
| final int oldLength = oldText.length(); |
| final int newLength = newText.length(); |
| |
| int startOffset = 0; |
| while ( |
| startOffset < oldLength && startOffset < newLength && |
| oldText.charAt(startOffset) == newText.charAt(startOffset) |
| ) { |
| startOffset++; |
| } |
| |
| int endOffset = oldLength; |
| while (true) { |
| if (endOffset <= startOffset) { |
| break; |
| } |
| final int idxInNew = newLength - (oldLength - endOffset) - 1; |
| if (idxInNew < startOffset) { |
| break; |
| } |
| |
| final char c1 = oldText.charAt(endOffset - 1); |
| final char c2 = newText.charAt(idxInNew); |
| if (c1 != c2) { |
| break; |
| } |
| endOffset--; |
| } |
| |
| return new ReplaceInfo(startOffset, endOffset, newText.substring(startOffset, newLength - (oldLength - endOffset))); |
| } |
| |
| /** |
| * @param rootContainer new container to be set as a root. |
| */ |
| private void setRootContainer(@NotNull final RadRootContainer rootContainer) { |
| if (myRootContainer != null) { |
| myLayeredPane.remove(myRootContainer.getDelegee()); |
| } |
| myRootContainer = rootContainer; |
| setDesignTimeInsets(2); |
| myLayeredPane.add(myRootContainer.getDelegee(), LAYER_COMPONENT); |
| |
| fireHierarchyChanged(); |
| } |
| |
| public void setDesignTimeInsets(final int insets) { |
| Integer oldInsets = (Integer)myRootContainer.getDelegee().getClientProperty(GridLayoutManager.DESIGN_TIME_INSETS); |
| if (oldInsets == null || oldInsets.intValue() != insets) { |
| myRootContainer.getDelegee().putClientProperty(GridLayoutManager.DESIGN_TIME_INSETS, insets); |
| revalidateRecursive(myRootContainer.getDelegee()); |
| } |
| } |
| |
| private static void revalidateRecursive(final JComponent component) { |
| for (Component child : component.getComponents()) { |
| if (child instanceof JComponent) { |
| revalidateRecursive((JComponent)child); |
| } |
| } |
| component.revalidate(); |
| component.repaint(); |
| } |
| |
| /** |
| * Creates and sets new <code>RadRootContainer</code> |
| * |
| * @param keepSelection if true, the GUI designer tries to preserve the selection state after reload. |
| */ |
| public void readFromFile(final boolean keepSelection) { |
| try { |
| ComponentPtr[] selection = null; |
| Map<String, String> tabbedPaneSelectedTabs = null; |
| if (keepSelection) { |
| selection = SelectionState.getSelection(this); |
| tabbedPaneSelectedTabs = saveTabbedPaneSelectedTabs(); |
| } |
| Locale oldLocale = null; |
| if (myRootContainer != null) { |
| oldLocale = myRootContainer.getStringDescriptorLocale(); |
| } |
| |
| final String text = myDocument.getText(); |
| |
| final ClassLoader classLoader = LoaderFactory.getInstance(getProject()).getLoader(myFile); |
| |
| final LwRootContainer rootContainer = Utils.getRootContainer(text, new CompiledClassPropertiesProvider(classLoader)); |
| final RadRootContainer container = XmlReader.createRoot(this, rootContainer, classLoader, oldLocale); |
| setRootContainer(container); |
| if (keepSelection) { |
| SelectionState.restoreSelection(this, selection); |
| restoreTabbedPaneSelectedTabs(tabbedPaneSelectedTabs); |
| } |
| myInvalid = false; |
| myCardLayout.show(myCardPanel, CARD_VALID); |
| refresh(); |
| } |
| catch (Exception exc) { |
| Throwable original = exc; |
| while (original instanceof InvocationTargetException) { |
| original = original.getCause(); |
| } |
| showInvalidCard(original); |
| } |
| catch (final LinkageError exc) { |
| showInvalidCard(exc); |
| } |
| } |
| |
| private void showInvalidCard(final Throwable exc) { |
| LOG.info(exc); |
| // setting fictive container |
| setRootContainer(new RadRootContainer(this, "0")); |
| myFormInvalidLabel.setText(UIDesignerBundle.message("error.form.file.is.invalid.message", FormEditingUtil.getExceptionMessage(exc))); |
| myInvalid = true; |
| myCardLayout.show(myCardPanel, CARD_INVALID); |
| repaint(); |
| } |
| |
| public boolean isFormInvalid() { |
| return myInvalid; |
| } |
| |
| private Map<String, String> saveTabbedPaneSelectedTabs() { |
| final Map<String, String> result = new HashMap<String, String>(); |
| FormEditingUtil.iterate(getRootContainer(), new FormEditingUtil.ComponentVisitor() { |
| public boolean visit(final IComponent component) { |
| if (component instanceof RadTabbedPane) { |
| RadTabbedPane tabbedPane = (RadTabbedPane)component; |
| RadComponent c = tabbedPane.getSelectedTab(); |
| if (c != null) { |
| result.put(tabbedPane.getId(), c.getId()); |
| } |
| } |
| return true; |
| } |
| }); |
| return result; |
| } |
| |
| private void restoreTabbedPaneSelectedTabs(final Map<String, String> tabbedPaneSelectedTabs) { |
| FormEditingUtil.iterate(getRootContainer(), new FormEditingUtil.ComponentVisitor() { |
| public boolean visit(final IComponent component) { |
| if (component instanceof RadTabbedPane) { |
| RadTabbedPane tabbedPane = (RadTabbedPane)component; |
| String selectedTabId = tabbedPaneSelectedTabs.get(tabbedPane.getId()); |
| if (selectedTabId != null) { |
| for (RadComponent c : tabbedPane.getComponents()) { |
| if (c.getId().equals(selectedTabId)) { |
| tabbedPane.selectTab(c); |
| break; |
| } |
| } |
| } |
| } |
| return true; |
| } |
| }); |
| } |
| |
| public JComponent getPreferredFocusedComponent() { |
| if (myValidCard.isVisible()) { |
| return myGlassLayer; |
| } |
| else { |
| return myInvalidCard; |
| } |
| } |
| |
| public static void repaintLayeredPane(final RadComponent component) { |
| final GuiEditor uiEditor = (GuiEditor)SwingUtilities.getAncestorOfClass(GuiEditor.class, component.getDelegee()); |
| if (uiEditor != null) { |
| uiEditor.repaintLayeredPane(); |
| } |
| } |
| |
| public boolean isShowGrid() { |
| return myShowGrid; |
| } |
| |
| public void setShowGrid(final boolean showGrid) { |
| if (myShowGrid != showGrid) { |
| myShowGrid = showGrid; |
| repaint(); |
| } |
| } |
| |
| public boolean isShowComponentTags() { |
| return myShowComponentTags; |
| } |
| |
| public void setShowComponentTags(final boolean showComponentTags) { |
| if (myShowComponentTags != showComponentTags) { |
| myShowComponentTags = showComponentTags; |
| repaint(); |
| } |
| } |
| |
| public DesignDropTargetListener getDropTargetListener() { |
| return myDropTargetListener; |
| } |
| |
| @Nullable |
| public GridCaptionPanel getFocusedCaptionPanel() { |
| if (myHorzCaptionPanel.isFocusOwner()) { |
| return myHorzCaptionPanel; |
| } |
| else if (myVertCaptionPanel.isFocusOwner()) { |
| return myVertCaptionPanel; |
| } |
| return null; |
| } |
| |
| public boolean isUndoRedoInProgress() { |
| UndoManager undoManager = UndoManager.getInstance(getProject()); |
| return undoManager.isUndoInProgress() || undoManager.isRedoInProgress(); |
| } |
| |
| void hideIntentionHint() { |
| myQuickFixManager.hideIntentionHint(); |
| } |
| |
| public void showFormSource() { |
| EditorFactory editorFactory = EditorFactory.getInstance(); |
| |
| Editor editor = editorFactory.createViewer(myDocument, myProject); |
| |
| try { |
| ((EditorEx)editor).setHighlighter( |
| new LexerEditorHighlighter(new XmlFileHighlighter(), EditorColorsManager.getInstance().getGlobalScheme())); |
| |
| JComponent component = editor.getComponent(); |
| component.setPreferredSize(new Dimension(640, 480)); |
| |
| DialogBuilder dialog = new DialogBuilder(myProject); |
| |
| dialog.title("Form - " + myFile.getPresentableName()).dimensionKey("GuiDesigner.FormSource.Dialog"); |
| dialog.centerPanel(component).setPreferredFocusComponent(editor.getContentComponent()); |
| dialog.addOkAction(); |
| |
| dialog.show(); |
| } |
| finally { |
| editorFactory.releaseEditor(editor); |
| } |
| } |
| |
| private final class MyLayeredPane extends JBLayeredPane implements Scrollable { |
| /** |
| * All components allocate whole pane's area. |
| */ |
| public void doLayout() { |
| for (int i = getComponentCount() - 1; i >= 0; i--) { |
| final Component component = getComponent(i); |
| component.setBounds(0, 0, getWidth(), getHeight()); |
| } |
| } |
| |
| public Dimension getMinimumSize() { |
| return getPreferredSize(); |
| } |
| |
| public Dimension getPreferredSize() { |
| // make sure all components fit |
| int width = 0; |
| int height = 0; |
| for (int i = 0; i < myRootContainer.getComponentCount(); i++) { |
| final RadComponent component = myRootContainer.getComponent(i); |
| width = Math.max(width, component.getX() + component.getWidth()); |
| height = Math.max(height, component.getY() + component.getHeight()); |
| } |
| |
| width += 50; |
| height += 40; |
| |
| Rectangle bounds = myScrollPane.getViewport().getBounds(); |
| |
| return new Dimension(Math.max(width, bounds.width), Math.max(height, bounds.height)); |
| } |
| |
| public Dimension getPreferredScrollableViewportSize() { |
| return getPreferredSize(); |
| } |
| |
| public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { |
| return 10; |
| } |
| |
| public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { |
| if (orientation == SwingConstants.HORIZONTAL) { |
| return visibleRect.width - 10; |
| } |
| return visibleRect.height - 10; |
| } |
| |
| public boolean getScrollableTracksViewportWidth() { |
| return false; |
| } |
| |
| public boolean getScrollableTracksViewportHeight() { |
| return false; |
| } |
| } |
| |
| /** |
| * Action works only if we are not editing something in the property inspector |
| */ |
| private final class CancelCurrentOperationAction extends AnAction { |
| public void actionPerformed(final AnActionEvent e) { |
| myProcessor.cancelOperation(); |
| myQuickFixManager.hideIntentionHint(); |
| } |
| |
| public void update(final AnActionEvent e) { |
| PropertyInspector inspector = DesignerToolWindowManager.getInstance(GuiEditor.this).getPropertyInspector(); |
| e.getPresentation().setEnabled(inspector != null && !inspector.isEditing()); |
| } |
| } |
| |
| /** |
| * Allows "DEL" button to work through the standard mechanism |
| */ |
| private final class MyDeleteProvider implements DeleteProvider { |
| public void deleteElement(@NotNull final DataContext dataContext) { |
| if (!GuiEditor.this.ensureEditable()) { |
| return; |
| } |
| CommandProcessor.getInstance().executeCommand(getProject(), new Runnable() { |
| public void run() { |
| FormEditingUtil.deleteSelection(GuiEditor.this); |
| } |
| }, UIDesignerBundle.message("command.delete.selection"), null); |
| } |
| |
| public boolean canDeleteElement(@NotNull final DataContext dataContext) { |
| return |
| !DesignerToolWindowManager.getInstance(GuiEditor.this).getPropertyInspector().isEditing() && |
| !myInplaceEditingLayer.isEditing() && |
| FormEditingUtil.canDeleteSelection(GuiEditor.this); |
| } |
| } |
| |
| /** |
| * Listens PSI event and update error highlighting in the UI editor |
| */ |
| private final class MyPsiTreeChangeListener extends PsiTreeChangeAdapter { |
| private final Alarm myAlarm; |
| private final MyRefreshPropertiesRequest myRefreshPropertiesRequest = new MyRefreshPropertiesRequest(); |
| private final MySynchronizeRequest mySynchronizeRequest = new MySynchronizeRequest(true); |
| |
| public MyPsiTreeChangeListener() { |
| myAlarm = new Alarm(); |
| } |
| |
| /** |
| * Cancels all pending update requests. You have to cancel all pending requests |
| * to not access to closed project. |
| */ |
| public void dispose() { |
| myAlarm.cancelAllRequests(); |
| } |
| |
| public void childAdded(@NotNull final PsiTreeChangeEvent event) { |
| handleEvent(event); |
| } |
| |
| public void childMoved(@NotNull final PsiTreeChangeEvent event) { |
| handleEvent(event); |
| } |
| |
| public void childrenChanged(@NotNull final PsiTreeChangeEvent event) { |
| handleEvent(event); |
| } |
| |
| public void childRemoved(@NotNull PsiTreeChangeEvent event) { |
| handleEvent(event); |
| } |
| |
| public void childReplaced(@NotNull PsiTreeChangeEvent event) { |
| handleEvent(event); |
| } |
| |
| public void propertyChanged(@NotNull final PsiTreeChangeEvent event) { |
| if (PsiTreeChangeEvent.PROP_ROOTS.equals(event.getPropertyName())) { |
| myAlarm.cancelRequest(myRefreshPropertiesRequest); |
| myAlarm.addRequest(myRefreshPropertiesRequest, 500, ModalityState.stateForComponent(GuiEditor.this)); |
| } |
| } |
| |
| private void handleEvent(final PsiTreeChangeEvent event) { |
| if (event.getParent() != null) { |
| PsiFile containingFile = event.getParent().getContainingFile(); |
| if (containingFile instanceof PropertiesFile) { |
| LOG.debug("Received PSI change event for properties file"); |
| myAlarm.cancelRequest(myRefreshPropertiesRequest); |
| myAlarm.addRequest(myRefreshPropertiesRequest, 500, ModalityState.stateForComponent(GuiEditor.this)); |
| } |
| else if (containingFile instanceof PsiPlainTextFile && containingFile.getFileType().equals(StdFileTypes.GUI_DESIGNER_FORM)) { |
| // quick check if relevant |
| String resourceName = FormEditingUtil.buildResourceName(containingFile); |
| if (myDocument.getText().indexOf(resourceName) >= 0) { |
| LOG.debug("Received PSI change event for nested form"); |
| // TODO[yole]: handle multiple nesting |
| myAlarm.cancelRequest(mySynchronizeRequest); |
| myAlarm.addRequest(mySynchronizeRequest, 500, ModalityState.stateForComponent(GuiEditor.this)); |
| } |
| } |
| } |
| } |
| } |
| |
| private class MySynchronizeRequest implements Runnable { |
| private final boolean myKeepSelection; |
| |
| public MySynchronizeRequest(final boolean keepSelection) { |
| myKeepSelection = keepSelection; |
| } |
| |
| public void run() { |
| if (getModule().isDisposed()) { |
| return; |
| } |
| Project project = getProject(); |
| if (project.isDisposed()) { |
| return; |
| } |
| LOG.debug("Synchronizing GUI editor " + myFile.getName() + " to document"); |
| PsiDocumentManager.getInstance(project).commitDocument(myDocument); |
| readFromFile(myKeepSelection); |
| } |
| } |
| |
| private class MyRefreshPropertiesRequest implements Runnable { |
| public void run() { |
| if (!getModule().isDisposed() && !getProject().isDisposed()) { |
| refreshProperties(); |
| } |
| } |
| } |
| |
| public void paletteKeyPressed(KeyEvent e) { |
| if (e.getKeyCode() == KeyEvent.VK_SHIFT && PaletteToolWindowManager.getInstance(this).getActiveItem(ComponentItem.class) != null) { |
| setDesignTimeInsets(12); |
| } |
| } |
| |
| public void paletteKeyReleased(KeyEvent e) { |
| if (e.getKeyCode() == KeyEvent.VK_SHIFT) { |
| setDesignTimeInsets(2); |
| } |
| } |
| |
| public void paletteDropActionChanged(int gestureModifiers) { |
| if ((gestureModifiers & InputEvent.SHIFT_MASK) != 0) { |
| setDesignTimeInsets(12); |
| } |
| else { |
| setDesignTimeInsets(2); |
| } |
| } |
| |
| public void paletteValueChanged(ListSelectionEvent e) { |
| if (PaletteToolWindowManager.getInstance(this).getActiveItem() == null) { |
| myProcessor.cancelPaletteInsert(); |
| } |
| } |
| } |