diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiTreeBlock.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiTreeBlock.java | 946 |
1 files changed, 946 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiTreeBlock.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiTreeBlock.java new file mode 100644 index 000000000..d11b8a4c6 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/ui/tree/UiTreeBlock.java @@ -0,0 +1,946 @@ +/* + * 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; + } + } + } + +} |