blob: d06b382b184f646d3bc0edeede07a971022e1962 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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.android.tools.idea.uibuilder.editor;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.idea.configurations.Configuration;
import com.android.tools.idea.rendering.RenderResult;
import com.android.tools.idea.uibuilder.model.ModelListener;
import com.android.tools.idea.uibuilder.model.NlComponent;
import com.android.tools.idea.uibuilder.model.NlModel;
import com.android.tools.idea.uibuilder.model.SelectionModel;
import com.android.tools.idea.uibuilder.surface.DesignSurface;
import com.android.tools.idea.uibuilder.surface.DesignSurfaceListener;
import com.android.tools.idea.uibuilder.surface.ScreenView;
import com.intellij.designer.DesignerEditorPanelFacade;
import com.intellij.designer.LightFillLayout;
import com.intellij.designer.LightToolWindow;
import com.intellij.designer.LightToolWindowManager;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.CaretModel;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.openapi.editor.event.CaretEvent;
import com.intellij.openapi.editor.event.CaretListener;
import com.intellij.openapi.fileEditor.TextEditor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.ThreeComponentsSplitter;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.psi.PsiFile;
import com.intellij.psi.xml.XmlFile;
import org.jetbrains.android.facet.AndroidFacet;
import javax.swing.*;
import java.util.Collections;
import java.util.List;
public class NlPreviewForm implements Disposable, CaretListener, DesignerEditorPanelFacade {
private final NlPreviewManager myManager;
private final DesignSurface mySurface;
private final ThreeComponentsSplitter myContentSplitter;
private boolean myUseInteractiveSelector = true;
private boolean myIgnoreListener;
private RenderResult myRenderResult;
private XmlFile myFile;
private TextEditor myEditor;
private CaretModel myCaretModel;
public NlPreviewForm(NlPreviewManager manager) {
myManager = manager;
mySurface = new DesignSurface(manager.getProject());
mySurface.setCentered(true);
mySurface.setScreenMode(DesignSurface.ScreenMode.SCREEN_ONLY);
mySurface.addListener(new DesignSurfaceListener() {
@Override
public void componentSelectionChanged(@NonNull DesignSurface surface, @NonNull List<NlComponent> newSelection) {
assert surface == mySurface; // We're maintaining the listener per surface
// Allow only one component
NlComponent component = newSelection.size() == 1 ? newSelection.get(0) : null;
selectComponent(component);
}
@Override
public void screenChanged(@NonNull DesignSurface surface, @Nullable ScreenView screenView) {
}
@Override
public void modelChanged(@NonNull DesignSurface surface, @Nullable NlModel model) {
}
});
myContentSplitter = new ThreeComponentsSplitter();
// The {@link LightFillLayout} provides the UI for the minimized forms of the {@link LightToolWindow}
// used for the palette and the structure/properties panes.
JPanel contentPanel = new JPanel(new LightFillLayout());
JComponent toolbar = NlEditorPanel.createToolbar(mySurface);
contentPanel.add(toolbar);
contentPanel.add(mySurface);
myContentSplitter.setDividerWidth(0);
myContentSplitter.setDividerMouseZoneSize(Registry.intValue("ide.splitter.mouseZone"));
myContentSplitter.setInnerComponent(contentPanel);
Project project = myManager.getProject();
NlPaletteManager paletteManager = NlPaletteManager.get(project);
PropertiesComponent properties = PropertiesComponent.getInstance(project);
// Keep in sync with LightToolWindow#myShowStateKey's logic
String key = LightToolWindowManager.EDITOR_MODE + paletteManager.getVisibilityKeyName(this) + ".SHOW";
boolean showing = properties.getBoolean(key, true);
if (showing) {
properties.setValue(key, Boolean.toString(false));
}
paletteManager.bind(this);
}
private void setEditor(@Nullable TextEditor editor) {
if (editor != myEditor) {
myEditor = editor;
if (myCaretModel != null) {
myCaretModel.removeCaretListener(this);
myCaretModel = null;
}
if (editor != null) {
myCaretModel = myEditor.getEditor().getCaretModel();
myCaretModel.addCaretListener(this);
}
}
}
private void selectComponent(@Nullable NlComponent component) {
ScreenView screenView = mySurface.getCurrentScreenView();
if (screenView == null) {
return;
}
if (myEditor != null && component != null && component.getTag().isValid() && myUseInteractiveSelector && !myIgnoreListener) {
int offset = component.getTag().getTextOffset();
if (offset != -1) {
Editor editor = myEditor.getEditor();
myIgnoreListener = true;
try {
screenView.getSelectionModel().setSelection(Collections.singletonList(component));
editor.getCaretModel().moveToOffset(offset);
editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
} finally {
myIgnoreListener = false;
}
}
}
}
private void updateCaret() {
if (myCaretModel != null && !myIgnoreListener && myUseInteractiveSelector) {
ScreenView screenView = mySurface.getCurrentScreenView();
if (screenView != null) {
int offset = myCaretModel.getOffset();
if (offset != -1) {
List<NlComponent> views = screenView.getModel().findByOffset(offset);
if (views != null && views.size() == 1 && views.get(0).isRoot()) {
views = null;
}
try {
myIgnoreListener = true;
SelectionModel selectionModel = screenView.getSelectionModel();
selectionModel.setSelection(views != null ? views : Collections.<NlComponent>emptyList());
mySurface.repaint();
} finally {
myIgnoreListener = false;
}
}
}
}
}
@Nullable
public XmlFile getFile() {
return myFile;
}
@NonNull
public JPanel getContentPanel() {
return myContentSplitter;
}
@Override
public void dispose() {
}
public void setUseInteractiveSelector(boolean useInteractiveSelector) {
this.myUseInteractiveSelector = useInteractiveSelector;
}
private class Pending implements ModelListener, Runnable {
public final XmlFile file;
public final NlModel model;
public boolean valid = true;
public Pending(XmlFile file, NlModel model) {
this.file = file;
this.model = model;
model.addListener(this);
model.requestRenderAsap(); // on file switches, render as soon as possible; the delay is for edits
}
@Override
public void modelChanged(@NonNull NlModel model) {
}
@Override
public void modelRendered(@NonNull NlModel model) {
model.removeListener(this);
if (valid) {
valid = false;
ApplicationManager.getApplication().invokeLater(this);
}
}
public void invalidate() {
valid = false;
}
@Override
public void run() {
// This method applies the given pending update to the UI thread; this must be done from a read thread
ApplicationManager.getApplication().assertIsDispatchThread();
setActiveModel(model);
}
}
private Pending myPendingFile;
public boolean setFile(@Nullable PsiFile file) {
if (myPendingFile != null) {
if (file == myPendingFile) {
return false;
}
myPendingFile.invalidate();
} else if (file == myFile) {
return false;
}
AndroidFacet facet = file instanceof XmlFile ? AndroidFacet.getInstance(file) : null;
if (facet == null || file.getVirtualFile() == null) {
myPendingFile = null;
myFile = null;
setEditor(null);
} else {
XmlFile xmlFile = (XmlFile)file;
NlModel model = NlModel.create(mySurface, xmlFile.getProject(), facet, xmlFile);
model.setRenderDelay(800);
myPendingFile = new Pending(xmlFile, model);
}
return true;
}
public void setActiveModel(@Nullable NlModel model) {
ScreenView currentScreenView = mySurface.getCurrentScreenView();
if (currentScreenView != null) {
currentScreenView.getModel().deactivate();
}
if (model == null) {
setEditor(null);
myManager.setDesignSurface(null);
} else {
myFile = model.getFile();
mySurface.setModel(model);
mySurface.zoomToFit();
setEditor(myManager.getActiveLayoutXmlEditor());
model.activate();
myManager.setDesignSurface(mySurface);
}
}
@Nullable
public RenderResult getRenderResult() {
return myRenderResult;
}
public void setRenderResult(@NonNull RenderResult renderResult) {
myRenderResult = renderResult;
}
@Nullable
public Configuration getConfiguration() {
ScreenView screenView = mySurface.getCurrentScreenView();
if (screenView != null) {
return screenView.getModel().getConfiguration();
}
return null;
}
@NonNull
public DesignSurface getSurface() {
return mySurface;
}
// ---- Implements CaretListener ----
@Override
public void caretPositionChanged(CaretEvent e) {
if (!myIgnoreListener) {
updateCaret();
// TODO: implement
//ActionBarHandler.showMenu(false, myContext, true);
}
}
@Override
public void caretAdded(CaretEvent e) {
}
@Override
public void caretRemoved(CaretEvent e) {
}
/** Minimize the palette tool window, if possible */
public void minimizePalette() {
if (myToolWindow != null) {
try {
// When LightToolWindow#minimize() is added to the base platform and upstreamed,
// replace this:
LightToolWindow.class.getDeclaredMethod("minimize").invoke(myToolWindow);
// with myToolWindow.minimize();
} catch (Exception ignore) {
}
}
}
// ---- Implements DesignerEditorPanelFacade ----
@Override
public Object getClientProperty(Object key) {
return myContentSplitter.getClientProperty(key);
}
private LightToolWindow myToolWindow;
@Override
public void putClientProperty(Object key, Object value) {
Project project = myManager.getProject();
NlPaletteManager paletteManager = NlPaletteManager.get(project);
String paletteKey = paletteManager.getComponentName();
myContentSplitter.putClientProperty(key, value);
if (key.equals(paletteKey)) {
myToolWindow = (LightToolWindow) value;
}
}
@Override
public ThreeComponentsSplitter getContentSplitter() {
return myContentSplitter;
}
}