| /* |
| * Copyright (C) 2007 The Android Open Source Project |
| * |
| * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php |
| * |
| * 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.ide.eclipse.adt.internal.editors.ui.tree; |
| |
| import com.android.ide.eclipse.adt.AdtPlugin; |
| import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; |
| import com.android.ide.eclipse.adt.internal.editors.IconFactory; |
| import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; |
| import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper; |
| import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart; |
| import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener; |
| import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; |
| import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; |
| import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener; |
| import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener; |
| |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.jface.action.Action; |
| import org.eclipse.jface.action.IMenuListener; |
| import org.eclipse.jface.action.IMenuManager; |
| import org.eclipse.jface.action.MenuManager; |
| import org.eclipse.jface.action.Separator; |
| import org.eclipse.jface.action.ToolBarManager; |
| import org.eclipse.jface.viewers.ILabelProvider; |
| import org.eclipse.jface.viewers.ISelection; |
| import org.eclipse.jface.viewers.ISelectionChangedListener; |
| import org.eclipse.jface.viewers.ITreeSelection; |
| import org.eclipse.jface.viewers.SelectionChangedEvent; |
| import org.eclipse.jface.viewers.TreePath; |
| import org.eclipse.jface.viewers.TreeSelection; |
| import org.eclipse.jface.viewers.TreeViewer; |
| import org.eclipse.jface.viewers.Viewer; |
| import org.eclipse.jface.viewers.ViewerComparator; |
| import org.eclipse.jface.viewers.ViewerFilter; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.dnd.Clipboard; |
| import org.eclipse.swt.events.DisposeEvent; |
| import org.eclipse.swt.events.DisposeListener; |
| import org.eclipse.swt.events.SelectionAdapter; |
| import org.eclipse.swt.events.SelectionEvent; |
| import org.eclipse.swt.layout.GridData; |
| import org.eclipse.swt.layout.GridLayout; |
| import org.eclipse.swt.widgets.Button; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.swt.widgets.Control; |
| import org.eclipse.swt.widgets.Menu; |
| import org.eclipse.swt.widgets.ToolBar; |
| import org.eclipse.swt.widgets.Tree; |
| import org.eclipse.ui.forms.DetailsPart; |
| import org.eclipse.ui.forms.IDetailsPage; |
| import org.eclipse.ui.forms.IDetailsPageProvider; |
| import org.eclipse.ui.forms.IManagedForm; |
| import org.eclipse.ui.forms.MasterDetailsBlock; |
| import org.eclipse.ui.forms.widgets.FormToolkit; |
| import org.eclipse.ui.forms.widgets.Section; |
| |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| |
| /** |
| * {@link UiTreeBlock} is a {@link MasterDetailsBlock} which displays a tree view for |
| * a specific set of {@link UiElementNode}. |
| * <p/> |
| * For a given UI element node, the tree view displays all first-level children that |
| * match a given type (given by an {@link ElementDescriptor}. All children from these |
| * nodes are also displayed. |
| * <p/> |
| * In the middle next to the tree are some controls to add or delete tree nodes. |
| * On the left is a details part that displays all the visible UI attributes for a given |
| * selected UI element node. |
| */ |
| public final class UiTreeBlock extends MasterDetailsBlock implements ICommitXml { |
| |
| /** Height hint for the tree view. Helps the grid layout resize properly on smaller screens. */ |
| private static final int TREE_HEIGHT_HINT = 50; |
| |
| /** Container editor */ |
| AndroidXmlEditor mEditor; |
| /** The root {@link UiElementNode} which contains all the elements that are to be |
| * manipulated by this tree view. In general this is the manifest UI node. */ |
| private UiElementNode mUiRootNode; |
| /** The descriptor of the elements to be displayed as root in this tree view. All elements |
| * of the same type in the root will be displayed. Can be null or empty to mean everything |
| * can be displayed. */ |
| private ElementDescriptor[] mDescriptorFilters; |
| /** The title for the master-detail part (displayed on the top "tab" on top of the tree) */ |
| private String mTitle; |
| /** The description for the master-detail part (displayed on top of the tree view) */ |
| private String mDescription; |
| /** The master-detail part, composed of a main tree and an auxiliary detail part */ |
| private ManifestSectionPart mMasterPart; |
| /** The tree viewer in the master-detail part */ |
| private TreeViewer mTreeViewer; |
| /** The "add" button for the tree view */ |
| private Button mAddButton; |
| /** The "remove" button for the tree view */ |
| private Button mRemoveButton; |
| /** The "up" button for the tree view */ |
| private Button mUpButton; |
| /** The "down" button for the tree view */ |
| private Button mDownButton; |
| /** The Managed Form used to create the master part */ |
| private IManagedForm mManagedForm; |
| /** Reference to the details part of the tree master block. */ |
| private DetailsPart mDetailsPart; |
| /** Reference to the clipboard for copy-paste */ |
| private Clipboard mClipboard; |
| /** Listener to refresh the tree viewer when the parent's node has been updated */ |
| private IUiUpdateListener mUiRefreshListener; |
| /** Listener to enable/disable the UI based on the application node's presence */ |
| private IUiUpdateListener mUiEnableListener; |
| /** An adapter/wrapper to use the add/remove/up/down tree edit actions. */ |
| private UiTreeActions mUiTreeActions; |
| /** |
| * True if the root node can be created on-demand (i.e. as needed as |
| * soon as children exist). False if an external entity controls the existence of the |
| * root node. In practise, this is false for the manifest application page (the actual |
| * "application" node is managed by the ApplicationToggle part) whereas it is true |
| * for all other tree pages. |
| */ |
| private final boolean mAutoCreateRoot; |
| |
| |
| /** |
| * Creates a new {@link MasterDetailsBlock} that will display all UI nodes matching the |
| * given filter in the given root node. |
| * |
| * @param editor The parent manifest editor. |
| * @param uiRootNode The root {@link UiElementNode} which contains all the elements that are |
| * to be manipulated by this tree view. In general this is the manifest UI node or the |
| * application UI node. This cannot be null. |
| * @param autoCreateRoot True if the root node can be created on-demand (i.e. as needed as |
| * soon as children exist). False if an external entity controls the existence of the |
| * root node. In practise, this is false for the manifest application page (the actual |
| * "application" node is managed by the ApplicationToggle part) whereas it is true |
| * for all other tree pages. |
| * @param descriptorFilters A list of descriptors of the elements to be displayed as root in |
| * this tree view. Use null or an empty list to accept any kind of node. |
| * @param title Title for the section |
| * @param description Description for the section |
| */ |
| public UiTreeBlock(AndroidXmlEditor editor, |
| UiElementNode uiRootNode, |
| boolean autoCreateRoot, |
| ElementDescriptor[] descriptorFilters, |
| String title, |
| String description) { |
| mEditor = editor; |
| mUiRootNode = uiRootNode; |
| mAutoCreateRoot = autoCreateRoot; |
| mDescriptorFilters = descriptorFilters; |
| mTitle = title; |
| mDescription = description; |
| } |
| |
| /** @returns The container editor */ |
| AndroidXmlEditor getEditor() { |
| return mEditor; |
| } |
| |
| /** @returns The reference to the clipboard for copy-paste */ |
| Clipboard getClipboard() { |
| return mClipboard; |
| } |
| |
| /** @returns The master-detail part, composed of a main tree and an auxiliary detail part */ |
| ManifestSectionPart getMasterPart() { |
| return mMasterPart; |
| } |
| |
| /** |
| * Returns the {@link UiElementNode} for the current model. |
| * <p/> |
| * This is used by the content provider attached to {@link #mTreeViewer} since |
| * the uiRootNode changes after each call to |
| * {@link #changeRootAndDescriptors(UiElementNode, ElementDescriptor[], boolean)}. |
| */ |
| public UiElementNode getRootNode() { |
| return mUiRootNode; |
| } |
| |
| @Override |
| protected void createMasterPart(final IManagedForm managedForm, Composite parent) { |
| FormToolkit toolkit = managedForm.getToolkit(); |
| |
| mManagedForm = managedForm; |
| mMasterPart = new ManifestSectionPart(parent, toolkit); |
| Section section = mMasterPart.getSection(); |
| section.setText(mTitle); |
| section.setDescription(mDescription); |
| section.setLayout(new GridLayout()); |
| section.setLayoutData(new GridData(GridData.FILL_BOTH)); |
| |
| Composite grid = SectionHelper.createGridLayout(section, toolkit, 2); |
| |
| Tree tree = createTreeViewer(toolkit, grid, managedForm); |
| createButtons(toolkit, grid); |
| createTreeContextMenu(tree); |
| createSectionActions(section, toolkit); |
| } |
| |
| private void createSectionActions(Section section, FormToolkit toolkit) { |
| ToolBarManager manager = new ToolBarManager(SWT.FLAT); |
| manager.removeAll(); |
| |
| ToolBar toolbar = manager.createControl(section); |
| section.setTextClient(toolbar); |
| |
| ElementDescriptor[] descs = mDescriptorFilters; |
| if (descs == null && mUiRootNode != null) { |
| descs = mUiRootNode.getDescriptor().getChildren(); |
| } |
| |
| if (descs != null && descs.length > 1) { |
| for (ElementDescriptor desc : descs) { |
| manager.add(new DescriptorFilterAction(desc)); |
| } |
| } |
| |
| manager.add(new TreeSortAction()); |
| |
| manager.update(true /*force*/); |
| } |
| |
| /** |
| * Creates the tree and its viewer |
| * @return The tree control |
| */ |
| private Tree createTreeViewer(FormToolkit toolkit, Composite grid, |
| final IManagedForm managedForm) { |
| // Note: we *could* use a FilteredTree instead of the Tree+TreeViewer here. |
| // However the class must be adapted to create an adapted toolkit tree. |
| final Tree tree = toolkit.createTree(grid, SWT.MULTI); |
| GridData gd = new GridData(GridData.FILL_BOTH); |
| gd.widthHint = AndroidXmlEditor.TEXT_WIDTH_HINT; |
| gd.heightHint = TREE_HEIGHT_HINT; |
| tree.setLayoutData(gd); |
| |
| mTreeViewer = new TreeViewer(tree); |
| mTreeViewer.setContentProvider(new UiModelTreeContentProvider(mUiRootNode, mDescriptorFilters)); |
| mTreeViewer.setLabelProvider(new UiModelTreeLabelProvider()); |
| mTreeViewer.setInput("unused"); //$NON-NLS-1$ |
| |
| // Create a listener that reacts to selections on the tree viewer. |
| // When a selection is made, ask the managed form to propagate an event to |
| // all parts in the managed form. |
| // This is picked up by UiElementDetail.selectionChanged(). |
| mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() { |
| @Override |
| public void selectionChanged(SelectionChangedEvent event) { |
| managedForm.fireSelectionChanged(mMasterPart, event.getSelection()); |
| adjustTreeButtons(event.getSelection()); |
| } |
| }); |
| |
| // Create three listeners: |
| // - One to refresh the tree viewer when the parent's node has been updated |
| // - One to refresh the tree viewer when the framework resources have changed |
| // - One to enable/disable the UI based on the application node's presence. |
| mUiRefreshListener = new IUiUpdateListener() { |
| @Override |
| public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) { |
| mTreeViewer.refresh(); |
| } |
| }; |
| |
| mUiEnableListener = new IUiUpdateListener() { |
| @Override |
| public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) { |
| // The UiElementNode for the application XML node always exists, even |
| // if there is no corresponding XML node in the XML file. |
| // |
| // Normally, we enable the UI here if the XML node is not null. |
| // |
| // However if mAutoCreateRoot is true, the root node will be created on-demand |
| // so the tree/block is always enabled. |
| boolean exists = mAutoCreateRoot || (ui_node.getXmlNode() != null); |
| if (mMasterPart != null) { |
| Section section = mMasterPart.getSection(); |
| if (section.getEnabled() != exists) { |
| section.setEnabled(exists); |
| for (Control c : section.getChildren()) { |
| c.setEnabled(exists); |
| } |
| } |
| } |
| } |
| }; |
| |
| /** Listener to update the root node if the target of the file is changed because of a |
| * SDK location change or a project target change */ |
| final ITargetChangeListener targetListener = new TargetChangeListener() { |
| @Override |
| public IProject getProject() { |
| if (mEditor != null) { |
| return mEditor.getProject(); |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public void reload() { |
| // If a details part has been created, we need to "refresh" it too. |
| if (mDetailsPart != null) { |
| // The details part does not directly expose access to its internal |
| // page book. Instead it is possible to resize the page book to 0 and then |
| // back to its original value, which has the side effect of removing all |
| // existing cached pages. |
| int limit = mDetailsPart.getPageLimit(); |
| mDetailsPart.setPageLimit(0); |
| mDetailsPart.setPageLimit(limit); |
| } |
| // Refresh the tree, preserving the selection if possible. |
| mTreeViewer.refresh(); |
| } |
| }; |
| |
| // Setup the listeners |
| changeRootAndDescriptors(mUiRootNode, mDescriptorFilters, false /* refresh */); |
| |
| // Listen on resource framework changes to refresh the tree |
| AdtPlugin.getDefault().addTargetListener(targetListener); |
| |
| // Remove listeners when the tree widget gets disposed. |
| tree.addDisposeListener(new DisposeListener() { |
| @Override |
| public void widgetDisposed(DisposeEvent e) { |
| if (mUiRootNode != null) { |
| UiElementNode node = mUiRootNode.getUiParent() != null ? |
| mUiRootNode.getUiParent() : |
| mUiRootNode; |
| |
| if (node != null) { |
| node.removeUpdateListener(mUiRefreshListener); |
| } |
| mUiRootNode.removeUpdateListener(mUiEnableListener); |
| } |
| |
| AdtPlugin.getDefault().removeTargetListener(targetListener); |
| if (mClipboard != null) { |
| mClipboard.dispose(); |
| mClipboard = null; |
| } |
| } |
| }); |
| |
| // Get a new clipboard reference. It is disposed when the tree is disposed. |
| mClipboard = new Clipboard(tree.getDisplay()); |
| |
| return tree; |
| } |
| |
| /** |
| * Changes the UI root node and the descriptor filters of the tree. |
| * <p/> |
| * This removes the listeners attached to the old root node and reattaches them to the |
| * new one. |
| * |
| * @param uiRootNode The root {@link UiElementNode} which contains all the elements that are |
| * to be manipulated by this tree view. In general this is the manifest UI node or the |
| * application UI node. This cannot be null. |
| * @param descriptorFilters A list of descriptors of the elements to be displayed as root in |
| * this tree view. Use null or an empty list to accept any kind of node. |
| * @param forceRefresh If tree, forces the tree to refresh |
| */ |
| public void changeRootAndDescriptors(UiElementNode uiRootNode, |
| ElementDescriptor[] descriptorFilters, boolean forceRefresh) { |
| UiElementNode node; |
| |
| // Remove previous listeners if any |
| if (mUiRootNode != null) { |
| node = mUiRootNode.getUiParent() != null ? mUiRootNode.getUiParent() : mUiRootNode; |
| node.removeUpdateListener(mUiRefreshListener); |
| mUiRootNode.removeUpdateListener(mUiEnableListener); |
| } |
| |
| mUiRootNode = uiRootNode; |
| mDescriptorFilters = descriptorFilters; |
| |
| mTreeViewer.setContentProvider( |
| new UiModelTreeContentProvider(mUiRootNode, mDescriptorFilters)); |
| |
| // Listen on structural changes on the root node of the tree |
| // If the node has a parent, listen on the parent instead. |
| if (mUiRootNode != null) { |
| node = mUiRootNode.getUiParent() != null ? mUiRootNode.getUiParent() : mUiRootNode; |
| |
| if (node != null) { |
| node.addUpdateListener(mUiRefreshListener); |
| } |
| |
| // Use the root node to listen to its presence. |
| mUiRootNode.addUpdateListener(mUiEnableListener); |
| |
| // Initialize the enabled/disabled state |
| mUiEnableListener.uiElementNodeUpdated(mUiRootNode, null /* state, not used */); |
| } |
| |
| if (forceRefresh) { |
| mTreeViewer.refresh(); |
| } |
| |
| createSectionActions(mMasterPart.getSection(), mManagedForm.getToolkit()); |
| } |
| |
| /** |
| * Creates the buttons next to the tree. |
| */ |
| private void createButtons(FormToolkit toolkit, Composite grid) { |
| |
| mUiTreeActions = new UiTreeActions(); |
| |
| Composite button_grid = SectionHelper.createGridLayout(grid, toolkit, 1); |
| button_grid.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_BEGINNING)); |
| mAddButton = toolkit.createButton(button_grid, "Add...", SWT.PUSH); |
| SectionHelper.addControlTooltip(mAddButton, "Adds a new element."); |
| mAddButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL | |
| GridData.VERTICAL_ALIGN_BEGINNING)); |
| |
| mAddButton.addSelectionListener(new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| super.widgetSelected(e); |
| doTreeAdd(); |
| } |
| }); |
| |
| mRemoveButton = toolkit.createButton(button_grid, "Remove...", SWT.PUSH); |
| SectionHelper.addControlTooltip(mRemoveButton, "Removes an existing selected element."); |
| mRemoveButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); |
| |
| mRemoveButton.addSelectionListener(new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| super.widgetSelected(e); |
| doTreeRemove(); |
| } |
| }); |
| |
| mUpButton = toolkit.createButton(button_grid, "Up", SWT.PUSH); |
| SectionHelper.addControlTooltip(mRemoveButton, "Moves the selected element up."); |
| mUpButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); |
| |
| mUpButton.addSelectionListener(new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| super.widgetSelected(e); |
| doTreeUp(); |
| } |
| }); |
| |
| mDownButton = toolkit.createButton(button_grid, "Down", SWT.PUSH); |
| SectionHelper.addControlTooltip(mRemoveButton, "Moves the selected element down."); |
| mDownButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); |
| |
| mDownButton.addSelectionListener(new SelectionAdapter() { |
| @Override |
| public void widgetSelected(SelectionEvent e) { |
| super.widgetSelected(e); |
| doTreeDown(); |
| } |
| }); |
| |
| adjustTreeButtons(TreeSelection.EMPTY); |
| } |
| |
| private void createTreeContextMenu(Tree tree) { |
| MenuManager menuManager = new MenuManager(); |
| menuManager.setRemoveAllWhenShown(true); |
| menuManager.addMenuListener(new IMenuListener() { |
| /** |
| * The menu is about to be shown. The menu manager has already been |
| * requested to remove any existing menu item. This method gets the |
| * tree selection and if it is of the appropriate type it re-creates |
| * the necessary actions. |
| */ |
| @Override |
| public void menuAboutToShow(IMenuManager manager) { |
| ISelection selection = mTreeViewer.getSelection(); |
| if (!selection.isEmpty() && selection instanceof ITreeSelection) { |
| ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); |
| doCreateMenuAction(manager, selected); |
| return; |
| } |
| doCreateMenuAction(manager, null /* ui_node */); |
| } |
| }); |
| Menu contextMenu = menuManager.createContextMenu(tree); |
| tree.setMenu(contextMenu); |
| } |
| |
| /** |
| * Adds the menu actions to the context menu when the given UI node is selected in |
| * the tree view. |
| * |
| * @param manager The context menu manager |
| * @param selected The UI nodes selected in the tree. Can be null, in which case the root |
| * is to be modified. |
| */ |
| private void doCreateMenuAction(IMenuManager manager, ArrayList<UiElementNode> selected) { |
| if (selected != null) { |
| boolean hasXml = false; |
| for (UiElementNode uiNode : selected) { |
| if (uiNode.getXmlNode() != null) { |
| hasXml = true; |
| break; |
| } |
| } |
| |
| if (hasXml) { |
| manager.add(new CopyCutAction(getEditor(), getClipboard(), |
| null, selected, true /* cut */)); |
| manager.add(new CopyCutAction(getEditor(), getClipboard(), |
| null, selected, false /* cut */)); |
| |
| // Can't paste with more than one element selected (the selection is the target) |
| if (selected.size() <= 1) { |
| // Paste is not valid if it would add a second element on a terminal element |
| // which parent is a document -- an XML document can only have one child. This |
| // means paste is valid if the current UI node can have children or if the |
| // parent is not a document. |
| UiElementNode ui_root = selected.get(0).getUiRoot(); |
| if (ui_root.getDescriptor().hasChildren() || |
| !(ui_root.getUiParent() instanceof UiDocumentNode)) { |
| manager.add(new PasteAction(getEditor(), getClipboard(), selected.get(0))); |
| } |
| } |
| manager.add(new Separator()); |
| } |
| } |
| |
| // Append "add" and "remove" actions. They do the same thing as the add/remove |
| // buttons on the side. |
| IconFactory factory = IconFactory.getInstance(); |
| |
| // "Add" makes sense only if there's 0 or 1 item selected since the |
| // one selected item becomes the target. |
| if (selected == null || selected.size() <= 1) { |
| manager.add(new Action("Add...", factory.getImageDescriptor("add")) { //$NON-NLS-1$ |
| @Override |
| public void run() { |
| super.run(); |
| doTreeAdd(); |
| } |
| }); |
| } |
| |
| if (selected != null) { |
| if (selected != null) { |
| manager.add(new Action("Remove", factory.getImageDescriptor("delete")) { //$NON-NLS-1$ |
| @Override |
| public void run() { |
| super.run(); |
| doTreeRemove(); |
| } |
| }); |
| } |
| manager.add(new Separator()); |
| |
| manager.add(new Action("Up", factory.getImageDescriptor("up")) { //$NON-NLS-1$ |
| @Override |
| public void run() { |
| super.run(); |
| doTreeUp(); |
| } |
| }); |
| manager.add(new Action("Down", factory.getImageDescriptor("down")) { //$NON-NLS-1$ |
| @Override |
| public void run() { |
| super.run(); |
| doTreeDown(); |
| } |
| }); |
| } |
| } |
| |
| |
| /** |
| * This is called by the tree when a selection is made. |
| * It enables/disables the buttons associated with the tree depending on the current |
| * selection. |
| * |
| * @param selection The current tree selection (same as mTreeViewer.getSelection()) |
| */ |
| private void adjustTreeButtons(ISelection selection) { |
| mRemoveButton.setEnabled(!selection.isEmpty() && selection instanceof ITreeSelection); |
| mUpButton.setEnabled(canDoTreeUp(selection)); |
| mDownButton.setEnabled(canDoTreeDown(selection)); |
| } |
| |
| /** |
| * An adapter/wrapper to use the add/remove/up/down tree edit actions. |
| */ |
| private class UiTreeActions extends UiActions { |
| @Override |
| protected UiElementNode getRootNode() { |
| return mUiRootNode; |
| } |
| |
| @Override |
| protected void selectUiNode(UiElementNode uiNodeToSelect) { |
| // Select the new item |
| if (uiNodeToSelect != null) { |
| LinkedList<UiElementNode> segments = new LinkedList<UiElementNode>(); |
| for (UiElementNode ui_node = uiNodeToSelect; ui_node != mUiRootNode; |
| ui_node = ui_node.getUiParent()) { |
| segments.add(0, ui_node); |
| } |
| if (segments.size() > 0) { |
| mTreeViewer.setSelection(new TreeSelection(new TreePath(segments.toArray()))); |
| } else { |
| mTreeViewer.setSelection(null); |
| } |
| } |
| } |
| |
| @Override |
| public void commitPendingXmlChanges() { |
| commitManagedForm(); |
| } |
| } |
| |
| /** |
| * Filters an ITreeSelection to only keep the {@link UiElementNode}s (in case there's |
| * something else in there). |
| * |
| * @return A new list of {@link UiElementNode} with at least one item or null. |
| */ |
| private ArrayList<UiElementNode> filterSelection(ITreeSelection selection) { |
| ArrayList<UiElementNode> selected = new ArrayList<UiElementNode>(); |
| |
| for (Iterator<Object> it = selection.iterator(); it.hasNext(); ) { |
| Object selectedObj = it.next(); |
| |
| if (selectedObj instanceof UiElementNode) { |
| selected.add((UiElementNode) selectedObj); |
| } |
| } |
| |
| return selected.size() > 0 ? selected : null; |
| } |
| |
| /** |
| * Called when the "Add..." button next to the tree view is selected. |
| * |
| * Displays a selection dialog that lets the user select which kind of node |
| * to create, depending on the current selection. |
| */ |
| private void doTreeAdd() { |
| UiElementNode ui_node = mUiRootNode; |
| ISelection selection = mTreeViewer.getSelection(); |
| if (!selection.isEmpty() && selection instanceof ITreeSelection) { |
| ITreeSelection tree_selection = (ITreeSelection) selection; |
| Object first = tree_selection.getFirstElement(); |
| if (first != null && first instanceof UiElementNode) { |
| ui_node = (UiElementNode) first; |
| } |
| } |
| |
| mUiTreeActions.doAdd( |
| ui_node, |
| mDescriptorFilters, |
| mTreeViewer.getControl().getShell(), |
| (ILabelProvider) mTreeViewer.getLabelProvider()); |
| } |
| |
| /** |
| * Called when the "Remove" button is selected. |
| * |
| * If the tree has a selection, remove it. |
| * This simply deletes the XML node attached to the UI node: when the XML model fires the |
| * update event, the tree will get refreshed. |
| */ |
| protected void doTreeRemove() { |
| ISelection selection = mTreeViewer.getSelection(); |
| if (!selection.isEmpty() && selection instanceof ITreeSelection) { |
| ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); |
| mUiTreeActions.doRemove(selected, mTreeViewer.getControl().getShell()); |
| } |
| } |
| |
| /** |
| * Called when the "Up" button is selected. |
| * <p/> |
| * If the tree has a selection, move it up, either in the child list or as the last child |
| * of the previous parent. |
| */ |
| protected void doTreeUp() { |
| ISelection selection = mTreeViewer.getSelection(); |
| if (!selection.isEmpty() && selection instanceof ITreeSelection) { |
| ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); |
| mUiTreeActions.doUp(selected, mDescriptorFilters); |
| } |
| } |
| |
| /** |
| * Checks whether the "up" action can be done on the current selection. |
| * |
| * @param selection The current tree selection. |
| * @return True if all the selected nodes can be moved up. |
| */ |
| protected boolean canDoTreeUp(ISelection selection) { |
| if (!selection.isEmpty() && selection instanceof ITreeSelection) { |
| ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); |
| return mUiTreeActions.canDoUp(selected, mDescriptorFilters); |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Called when the "Down" button is selected. |
| * |
| * If the tree has a selection, move it down, either in the same child list or as the |
| * first child of the next parent. |
| */ |
| protected void doTreeDown() { |
| ISelection selection = mTreeViewer.getSelection(); |
| if (!selection.isEmpty() && selection instanceof ITreeSelection) { |
| ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); |
| mUiTreeActions.doDown(selected, mDescriptorFilters); |
| } |
| } |
| |
| /** |
| * Checks whether the "down" action can be done on the current selection. |
| * |
| * @param selection The current tree selection. |
| * @return True if all the selected nodes can be moved down. |
| */ |
| protected boolean canDoTreeDown(ISelection selection) { |
| if (!selection.isEmpty() && selection instanceof ITreeSelection) { |
| ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection); |
| return mUiTreeActions.canDoDown(selected, mDescriptorFilters); |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Commits the current managed form (the one associated with our master part). |
| * As a side effect, this will commit the current UiElementDetails page. |
| */ |
| void commitManagedForm() { |
| if (mManagedForm != null) { |
| mManagedForm.commit(false /* onSave */); |
| } |
| } |
| |
| /* Implements ICommitXml for CopyCutAction */ |
| @Override |
| public void commitPendingXmlChanges() { |
| commitManagedForm(); |
| } |
| |
| @Override |
| protected void createToolBarActions(IManagedForm managedForm) { |
| // Pass. Not used, toolbar actions are defined by createSectionActions(). |
| } |
| |
| @Override |
| protected void registerPages(DetailsPart inDetailsPart) { |
| // Keep a reference on the details part (the super class doesn't provide a getter |
| // for it.) |
| mDetailsPart = inDetailsPart; |
| |
| // The page selection mechanism does not use pages registered by association with |
| // a node class. Instead it uses a custom details page provider that provides a |
| // new UiElementDetail instance for each node instance. A limit of 5 pages is |
| // then set (the value is arbitrary but should be reasonable) for the internal |
| // page book. |
| inDetailsPart.setPageLimit(5); |
| |
| final UiTreeBlock tree = this; |
| |
| inDetailsPart.setPageProvider(new IDetailsPageProvider() { |
| @Override |
| public IDetailsPage getPage(Object key) { |
| if (key instanceof UiElementNode) { |
| return new UiElementDetail(tree); |
| } |
| return null; |
| } |
| |
| @Override |
| public Object getPageKey(Object object) { |
| return object; // use node object as key |
| } |
| }); |
| } |
| |
| /** |
| * An alphabetic sort action for the tree viewer. |
| */ |
| private class TreeSortAction extends Action { |
| |
| private ViewerComparator mComparator; |
| |
| public TreeSortAction() { |
| super("Sorts elements alphabetically.", AS_CHECK_BOX); |
| setImageDescriptor(IconFactory.getInstance().getImageDescriptor("az_sort")); //$NON-NLS-1$ |
| |
| if (mTreeViewer != null) { |
| boolean is_sorted = mTreeViewer.getComparator() != null; |
| setChecked(is_sorted); |
| } |
| } |
| |
| /** |
| * Called when the button is selected. Toggles the tree viewer comparator. |
| */ |
| @Override |
| public void run() { |
| if (mTreeViewer == null) { |
| notifyResult(false /*success*/); |
| return; |
| } |
| |
| ViewerComparator comp = mTreeViewer.getComparator(); |
| if (comp != null) { |
| // Tree is currently sorted. |
| // Save currently comparator and remove it |
| mComparator = comp; |
| mTreeViewer.setComparator(null); |
| } else { |
| // Tree is not currently sorted. |
| // Reuse or add a new comparator. |
| if (mComparator == null) { |
| mComparator = new ViewerComparator(); |
| } |
| mTreeViewer.setComparator(mComparator); |
| } |
| |
| notifyResult(true /*success*/); |
| } |
| } |
| |
| /** |
| * A filter on descriptor for the tree viewer. |
| * <p/> |
| * The tree viewer will contain many of these actions and only one can be enabled at a |
| * given time. When no action is selected, everything is displayed. |
| * <p/> |
| * Since "radio"-like actions do not allow for unselecting all of them, we manually |
| * handle the exclusive radio button-like property: when an action is selected, it manually |
| * removes all other actions as needed. |
| */ |
| private class DescriptorFilterAction extends Action { |
| |
| private final ElementDescriptor mDescriptor; |
| private ViewerFilter mFilter; |
| |
| public DescriptorFilterAction(ElementDescriptor descriptor) { |
| super(String.format("Displays only %1$s elements.", descriptor.getUiName()), |
| AS_CHECK_BOX); |
| |
| mDescriptor = descriptor; |
| setImageDescriptor(descriptor.getImageDescriptor()); |
| } |
| |
| /** |
| * Called when the button is selected. |
| * <p/> |
| * Find any existing {@link DescriptorFilter}s and remove them. Install ours. |
| */ |
| @Override |
| public void run() { |
| super.run(); |
| |
| if (isChecked()) { |
| if (mFilter == null) { |
| // create filter when required |
| mFilter = new DescriptorFilter(this); |
| } |
| |
| // we add our filter first, otherwise the UI might show the full list |
| mTreeViewer.addFilter(mFilter); |
| |
| // Then remove the any other filters except ours. There should be at most |
| // one other filter, since that's how the actions are made to look like |
| // exclusive radio buttons. |
| for (ViewerFilter filter : mTreeViewer.getFilters()) { |
| if (filter instanceof DescriptorFilter && filter != mFilter) { |
| DescriptorFilterAction action = ((DescriptorFilter) filter).getAction(); |
| action.setChecked(false); |
| mTreeViewer.removeFilter(filter); |
| } |
| } |
| } else if (mFilter != null){ |
| mTreeViewer.removeFilter(mFilter); |
| } |
| } |
| |
| /** |
| * Filters the tree viewer for the given descriptor. |
| * <p/> |
| * The filter is linked to the action so that an action can iterate through the list |
| * of filters and un-select the actions. |
| */ |
| private class DescriptorFilter extends ViewerFilter { |
| |
| private final DescriptorFilterAction mAction; |
| |
| public DescriptorFilter(DescriptorFilterAction action) { |
| mAction = action; |
| } |
| |
| public DescriptorFilterAction getAction() { |
| return mAction; |
| } |
| |
| /** |
| * Returns true if an element should be displayed, that if the element or |
| * any of its parent matches the requested descriptor. |
| */ |
| @Override |
| public boolean select(Viewer viewer, Object parentElement, Object element) { |
| while (element instanceof UiElementNode) { |
| UiElementNode uiNode = (UiElementNode)element; |
| if (uiNode.getDescriptor() == mDescriptor) { |
| return true; |
| } |
| element = uiNode.getUiParent(); |
| } |
| return false; |
| } |
| } |
| } |
| |
| } |