Home | History | Annotate | Download | only in tree
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.editors.ui.tree;
     18 
     19 import com.android.ide.eclipse.adt.AdtPlugin;
     20 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
     21 import com.android.ide.eclipse.adt.internal.editors.IconFactory;
     22 import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
     23 import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
     24 import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper.ManifestSectionPart;
     25 import com.android.ide.eclipse.adt.internal.editors.uimodel.IUiUpdateListener;
     26 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
     27 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
     28 import com.android.ide.eclipse.adt.internal.sdk.Sdk.ITargetChangeListener;
     29 import com.android.ide.eclipse.adt.internal.sdk.Sdk.TargetChangeListener;
     30 
     31 import org.eclipse.core.resources.IProject;
     32 import org.eclipse.jface.action.Action;
     33 import org.eclipse.jface.action.IMenuListener;
     34 import org.eclipse.jface.action.IMenuManager;
     35 import org.eclipse.jface.action.MenuManager;
     36 import org.eclipse.jface.action.Separator;
     37 import org.eclipse.jface.action.ToolBarManager;
     38 import org.eclipse.jface.viewers.ILabelProvider;
     39 import org.eclipse.jface.viewers.ISelection;
     40 import org.eclipse.jface.viewers.ISelectionChangedListener;
     41 import org.eclipse.jface.viewers.ITreeSelection;
     42 import org.eclipse.jface.viewers.SelectionChangedEvent;
     43 import org.eclipse.jface.viewers.TreePath;
     44 import org.eclipse.jface.viewers.TreeSelection;
     45 import org.eclipse.jface.viewers.TreeViewer;
     46 import org.eclipse.jface.viewers.Viewer;
     47 import org.eclipse.jface.viewers.ViewerComparator;
     48 import org.eclipse.jface.viewers.ViewerFilter;
     49 import org.eclipse.swt.SWT;
     50 import org.eclipse.swt.dnd.Clipboard;
     51 import org.eclipse.swt.events.DisposeEvent;
     52 import org.eclipse.swt.events.DisposeListener;
     53 import org.eclipse.swt.events.SelectionAdapter;
     54 import org.eclipse.swt.events.SelectionEvent;
     55 import org.eclipse.swt.layout.GridData;
     56 import org.eclipse.swt.layout.GridLayout;
     57 import org.eclipse.swt.widgets.Button;
     58 import org.eclipse.swt.widgets.Composite;
     59 import org.eclipse.swt.widgets.Control;
     60 import org.eclipse.swt.widgets.Menu;
     61 import org.eclipse.swt.widgets.ToolBar;
     62 import org.eclipse.swt.widgets.Tree;
     63 import org.eclipse.ui.forms.DetailsPart;
     64 import org.eclipse.ui.forms.IDetailsPage;
     65 import org.eclipse.ui.forms.IDetailsPageProvider;
     66 import org.eclipse.ui.forms.IManagedForm;
     67 import org.eclipse.ui.forms.MasterDetailsBlock;
     68 import org.eclipse.ui.forms.widgets.FormToolkit;
     69 import org.eclipse.ui.forms.widgets.Section;
     70 
     71 import java.util.ArrayList;
     72 import java.util.Iterator;
     73 import java.util.LinkedList;
     74 
     75 /**
     76  * {@link UiTreeBlock} is a {@link MasterDetailsBlock} which displays a tree view for
     77  * a specific set of {@link UiElementNode}.
     78  * <p/>
     79  * For a given UI element node, the tree view displays all first-level children that
     80  * match a given type (given by an {@link ElementDescriptor}. All children from these
     81  * nodes are also displayed.
     82  * <p/>
     83  * In the middle next to the tree are some controls to add or delete tree nodes.
     84  * On the left is a details part that displays all the visible UI attributes for a given
     85  * selected UI element node.
     86  */
     87 public final class UiTreeBlock extends MasterDetailsBlock implements ICommitXml {
     88 
     89     /** Height hint for the tree view. Helps the grid layout resize properly on smaller screens. */
     90     private static final int TREE_HEIGHT_HINT = 50;
     91 
     92     /** Container editor */
     93     AndroidXmlEditor mEditor;
     94     /** The root {@link UiElementNode} which contains all the elements that are to be
     95      *  manipulated by this tree view. In general this is the manifest UI node. */
     96     private UiElementNode mUiRootNode;
     97     /** The descriptor of the elements to be displayed as root in this tree view. All elements
     98      *  of the same type in the root will be displayed. Can be null or empty to mean everything
     99      *  can be displayed. */
    100     private ElementDescriptor[] mDescriptorFilters;
    101     /** The title for the master-detail part (displayed on the top "tab" on top of the tree) */
    102     private String mTitle;
    103     /** The description for the master-detail part (displayed on top of the tree view) */
    104     private String mDescription;
    105     /** The master-detail part, composed of a main tree and an auxiliary detail part */
    106     private ManifestSectionPart mMasterPart;
    107     /** The tree viewer in the master-detail part */
    108     private TreeViewer mTreeViewer;
    109     /** The "add" button for the tree view */
    110     private Button mAddButton;
    111     /** The "remove" button for the tree view */
    112     private Button mRemoveButton;
    113     /** The "up" button for the tree view */
    114     private Button mUpButton;
    115     /** The "down" button for the tree view */
    116     private Button mDownButton;
    117     /** The Managed Form used to create the master part */
    118     private IManagedForm mManagedForm;
    119     /** Reference to the details part of the tree master block. */
    120     private DetailsPart mDetailsPart;
    121     /** Reference to the clipboard for copy-paste */
    122     private Clipboard mClipboard;
    123     /** Listener to refresh the tree viewer when the parent's node has been updated */
    124     private IUiUpdateListener mUiRefreshListener;
    125     /** Listener to enable/disable the UI based on the application node's presence */
    126     private IUiUpdateListener mUiEnableListener;
    127     /** An adapter/wrapper to use the add/remove/up/down tree edit actions. */
    128     private UiTreeActions mUiTreeActions;
    129     /**
    130      * True if the root node can be created on-demand (i.e. as needed as
    131      * soon as children exist). False if an external entity controls the existence of the
    132      * root node. In practise, this is false for the manifest application page (the actual
    133      * "application" node is managed by the ApplicationToggle part) whereas it is true
    134      * for all other tree pages.
    135      */
    136     private final boolean mAutoCreateRoot;
    137 
    138 
    139     /**
    140      * Creates a new {@link MasterDetailsBlock} that will display all UI nodes matching the
    141      * given filter in the given root node.
    142      *
    143      * @param editor The parent manifest editor.
    144      * @param uiRootNode The root {@link UiElementNode} which contains all the elements that are
    145      *        to be manipulated by this tree view. In general this is the manifest UI node or the
    146      *        application UI node. This cannot be null.
    147      * @param autoCreateRoot True if the root node can be created on-demand (i.e. as needed as
    148      *        soon as children exist). False if an external entity controls the existence of the
    149      *        root node. In practise, this is false for the manifest application page (the actual
    150      *        "application" node is managed by the ApplicationToggle part) whereas it is true
    151      *        for all other tree pages.
    152      * @param descriptorFilters A list of descriptors of the elements to be displayed as root in
    153      *        this tree view. Use null or an empty list to accept any kind of node.
    154      * @param title Title for the section
    155      * @param description Description for the section
    156      */
    157     public UiTreeBlock(AndroidXmlEditor editor,
    158             UiElementNode uiRootNode,
    159             boolean autoCreateRoot,
    160             ElementDescriptor[] descriptorFilters,
    161             String title,
    162             String description) {
    163         mEditor = editor;
    164         mUiRootNode = uiRootNode;
    165         mAutoCreateRoot = autoCreateRoot;
    166         mDescriptorFilters = descriptorFilters;
    167         mTitle = title;
    168         mDescription = description;
    169     }
    170 
    171     /** @returns The container editor */
    172     AndroidXmlEditor getEditor() {
    173         return mEditor;
    174     }
    175 
    176     /** @returns The reference to the clipboard for copy-paste */
    177     Clipboard getClipboard() {
    178         return mClipboard;
    179     }
    180 
    181     /** @returns The master-detail part, composed of a main tree and an auxiliary detail part */
    182     ManifestSectionPart getMasterPart() {
    183         return mMasterPart;
    184     }
    185 
    186     /**
    187      * Returns the {@link UiElementNode} for the current model.
    188      * <p/>
    189      * This is used by the content provider attached to {@link #mTreeViewer} since
    190      * the uiRootNode changes after each call to
    191      * {@link #changeRootAndDescriptors(UiElementNode, ElementDescriptor[], boolean)}.
    192      */
    193     public UiElementNode getRootNode() {
    194         return mUiRootNode;
    195     }
    196 
    197     @Override
    198     protected void createMasterPart(final IManagedForm managedForm, Composite parent) {
    199         FormToolkit toolkit = managedForm.getToolkit();
    200 
    201         mManagedForm = managedForm;
    202         mMasterPart = new ManifestSectionPart(parent, toolkit);
    203         Section section = mMasterPart.getSection();
    204         section.setText(mTitle);
    205         section.setDescription(mDescription);
    206         section.setLayout(new GridLayout());
    207         section.setLayoutData(new GridData(GridData.FILL_BOTH));
    208 
    209         Composite grid = SectionHelper.createGridLayout(section, toolkit, 2);
    210 
    211         Tree tree = createTreeViewer(toolkit, grid, managedForm);
    212         createButtons(toolkit, grid);
    213         createTreeContextMenu(tree);
    214         createSectionActions(section, toolkit);
    215     }
    216 
    217     private void createSectionActions(Section section, FormToolkit toolkit) {
    218         ToolBarManager manager = new ToolBarManager(SWT.FLAT);
    219         manager.removeAll();
    220 
    221         ToolBar toolbar = manager.createControl(section);
    222         section.setTextClient(toolbar);
    223 
    224         ElementDescriptor[] descs = mDescriptorFilters;
    225         if (descs == null && mUiRootNode != null) {
    226             descs = mUiRootNode.getDescriptor().getChildren();
    227         }
    228 
    229         if (descs != null && descs.length > 1) {
    230             for (ElementDescriptor desc : descs) {
    231                 manager.add(new DescriptorFilterAction(desc));
    232             }
    233         }
    234 
    235         manager.add(new TreeSortAction());
    236 
    237         manager.update(true /*force*/);
    238     }
    239 
    240     /**
    241      * Creates the tree and its viewer
    242      * @return The tree control
    243      */
    244     private Tree createTreeViewer(FormToolkit toolkit, Composite grid,
    245             final IManagedForm managedForm) {
    246         // Note: we *could* use a FilteredTree instead of the Tree+TreeViewer here.
    247         // However the class must be adapted to create an adapted toolkit tree.
    248         final Tree tree = toolkit.createTree(grid, SWT.MULTI);
    249         GridData gd = new GridData(GridData.FILL_BOTH);
    250         gd.widthHint = AndroidXmlEditor.TEXT_WIDTH_HINT;
    251         gd.heightHint = TREE_HEIGHT_HINT;
    252         tree.setLayoutData(gd);
    253 
    254         mTreeViewer = new TreeViewer(tree);
    255         mTreeViewer.setContentProvider(new UiModelTreeContentProvider(mUiRootNode, mDescriptorFilters));
    256         mTreeViewer.setLabelProvider(new UiModelTreeLabelProvider());
    257         mTreeViewer.setInput("unused"); //$NON-NLS-1$
    258 
    259         // Create a listener that reacts to selections on the tree viewer.
    260         // When a selection is made, ask the managed form to propagate an event to
    261         // all parts in the managed form.
    262         // This is picked up by UiElementDetail.selectionChanged().
    263         mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
    264             public void selectionChanged(SelectionChangedEvent event) {
    265                 managedForm.fireSelectionChanged(mMasterPart, event.getSelection());
    266                 adjustTreeButtons(event.getSelection());
    267             }
    268         });
    269 
    270         // Create three listeners:
    271         // - One to refresh the tree viewer when the parent's node has been updated
    272         // - One to refresh the tree viewer when the framework resources have changed
    273         // - One to enable/disable the UI based on the application node's presence.
    274         mUiRefreshListener = new IUiUpdateListener() {
    275             public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) {
    276                 mTreeViewer.refresh();
    277             }
    278         };
    279 
    280         mUiEnableListener = new IUiUpdateListener() {
    281             public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) {
    282                 // The UiElementNode for the application XML node always exists, even
    283                 // if there is no corresponding XML node in the XML file.
    284                 //
    285                 // Normally, we enable the UI here if the XML node is not null.
    286                 //
    287                 // However if mAutoCreateRoot is true, the root node will be created on-demand
    288                 // so the tree/block is always enabled.
    289                 boolean exists = mAutoCreateRoot || (ui_node.getXmlNode() != null);
    290                 if (mMasterPart != null) {
    291                     Section section = mMasterPart.getSection();
    292                     if (section.getEnabled() != exists) {
    293                         section.setEnabled(exists);
    294                         for (Control c : section.getChildren()) {
    295                             c.setEnabled(exists);
    296                         }
    297                     }
    298                 }
    299             }
    300         };
    301 
    302         /** Listener to update the root node if the target of the file is changed because of a
    303          * SDK location change or a project target change */
    304         final ITargetChangeListener targetListener = new TargetChangeListener() {
    305             @Override
    306             public IProject getProject() {
    307                 if (mEditor != null) {
    308                     return mEditor.getProject();
    309                 }
    310 
    311                 return null;
    312             }
    313 
    314             @Override
    315             public void reload() {
    316                 // If a details part has been created, we need to "refresh" it too.
    317                 if (mDetailsPart != null) {
    318                     // The details part does not directly expose access to its internal
    319                     // page book. Instead it is possible to resize the page book to 0 and then
    320                     // back to its original value, which has the side effect of removing all
    321                     // existing cached pages.
    322                     int limit = mDetailsPart.getPageLimit();
    323                     mDetailsPart.setPageLimit(0);
    324                     mDetailsPart.setPageLimit(limit);
    325                 }
    326                 // Refresh the tree, preserving the selection if possible.
    327                 mTreeViewer.refresh();
    328             }
    329         };
    330 
    331         // Setup the listeners
    332         changeRootAndDescriptors(mUiRootNode, mDescriptorFilters, false /* refresh */);
    333 
    334         // Listen on resource framework changes to refresh the tree
    335         AdtPlugin.getDefault().addTargetListener(targetListener);
    336 
    337         // Remove listeners when the tree widget gets disposed.
    338         tree.addDisposeListener(new DisposeListener() {
    339             public void widgetDisposed(DisposeEvent e) {
    340                 if (mUiRootNode != null) {
    341                     UiElementNode node = mUiRootNode.getUiParent() != null ?
    342                                             mUiRootNode.getUiParent() :
    343                                             mUiRootNode;
    344 
    345                     if (node != null) {
    346                         node.removeUpdateListener(mUiRefreshListener);
    347                     }
    348                     mUiRootNode.removeUpdateListener(mUiEnableListener);
    349                 }
    350 
    351                 AdtPlugin.getDefault().removeTargetListener(targetListener);
    352                 if (mClipboard != null) {
    353                     mClipboard.dispose();
    354                     mClipboard = null;
    355                 }
    356             }
    357         });
    358 
    359         // Get a new clipboard reference. It is disposed when the tree is disposed.
    360         mClipboard = new Clipboard(tree.getDisplay());
    361 
    362         return tree;
    363     }
    364 
    365     /**
    366      * Changes the UI root node and the descriptor filters of the tree.
    367      * <p/>
    368      * This removes the listeners attached to the old root node and reattaches them to the
    369      * new one.
    370      *
    371      * @param uiRootNode The root {@link UiElementNode} which contains all the elements that are
    372      *        to be manipulated by this tree view. In general this is the manifest UI node or the
    373      *        application UI node. This cannot be null.
    374      * @param descriptorFilters A list of descriptors of the elements to be displayed as root in
    375      *        this tree view. Use null or an empty list to accept any kind of node.
    376      * @param forceRefresh If tree, forces the tree to refresh
    377      */
    378     public void changeRootAndDescriptors(UiElementNode uiRootNode,
    379             ElementDescriptor[] descriptorFilters, boolean forceRefresh) {
    380         UiElementNode node;
    381 
    382         // Remove previous listeners if any
    383         if (mUiRootNode != null) {
    384             node = mUiRootNode.getUiParent() != null ? mUiRootNode.getUiParent() : mUiRootNode;
    385             node.removeUpdateListener(mUiRefreshListener);
    386             mUiRootNode.removeUpdateListener(mUiEnableListener);
    387         }
    388 
    389         mUiRootNode = uiRootNode;
    390         mDescriptorFilters = descriptorFilters;
    391 
    392         mTreeViewer.setContentProvider(
    393                 new UiModelTreeContentProvider(mUiRootNode, mDescriptorFilters));
    394 
    395         // Listen on structural changes on the root node of the tree
    396         // If the node has a parent, listen on the parent instead.
    397         if (mUiRootNode != null) {
    398             node = mUiRootNode.getUiParent() != null ? mUiRootNode.getUiParent() : mUiRootNode;
    399 
    400             if (node != null) {
    401                 node.addUpdateListener(mUiRefreshListener);
    402             }
    403 
    404             // Use the root node to listen to its presence.
    405             mUiRootNode.addUpdateListener(mUiEnableListener);
    406 
    407             // Initialize the enabled/disabled state
    408             mUiEnableListener.uiElementNodeUpdated(mUiRootNode, null /* state, not used */);
    409         }
    410 
    411         if (forceRefresh) {
    412             mTreeViewer.refresh();
    413         }
    414 
    415         createSectionActions(mMasterPart.getSection(), mManagedForm.getToolkit());
    416     }
    417 
    418     /**
    419      * Creates the buttons next to the tree.
    420      */
    421     private void createButtons(FormToolkit toolkit, Composite grid) {
    422 
    423         mUiTreeActions = new UiTreeActions();
    424 
    425         Composite button_grid = SectionHelper.createGridLayout(grid, toolkit, 1);
    426         button_grid.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_BEGINNING));
    427         mAddButton = toolkit.createButton(button_grid, "Add...", SWT.PUSH);
    428         SectionHelper.addControlTooltip(mAddButton, "Adds a new element.");
    429         mAddButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL |
    430                 GridData.VERTICAL_ALIGN_BEGINNING));
    431 
    432         mAddButton.addSelectionListener(new SelectionAdapter() {
    433             @Override
    434             public void widgetSelected(SelectionEvent e) {
    435                 super.widgetSelected(e);
    436                 doTreeAdd();
    437             }
    438         });
    439 
    440         mRemoveButton = toolkit.createButton(button_grid, "Remove...", SWT.PUSH);
    441         SectionHelper.addControlTooltip(mRemoveButton, "Removes an existing selected element.");
    442         mRemoveButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
    443 
    444         mRemoveButton.addSelectionListener(new SelectionAdapter() {
    445             @Override
    446             public void widgetSelected(SelectionEvent e) {
    447                 super.widgetSelected(e);
    448                 doTreeRemove();
    449             }
    450         });
    451 
    452         mUpButton = toolkit.createButton(button_grid, "Up", SWT.PUSH);
    453         SectionHelper.addControlTooltip(mRemoveButton, "Moves the selected element up.");
    454         mUpButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
    455 
    456         mUpButton.addSelectionListener(new SelectionAdapter() {
    457             @Override
    458             public void widgetSelected(SelectionEvent e) {
    459                 super.widgetSelected(e);
    460                 doTreeUp();
    461             }
    462         });
    463 
    464         mDownButton = toolkit.createButton(button_grid, "Down", SWT.PUSH);
    465         SectionHelper.addControlTooltip(mRemoveButton, "Moves the selected element down.");
    466         mDownButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
    467 
    468         mDownButton.addSelectionListener(new SelectionAdapter() {
    469             @Override
    470             public void widgetSelected(SelectionEvent e) {
    471                 super.widgetSelected(e);
    472                 doTreeDown();
    473             }
    474         });
    475 
    476         adjustTreeButtons(TreeSelection.EMPTY);
    477     }
    478 
    479     private void createTreeContextMenu(Tree tree) {
    480         MenuManager menuManager = new MenuManager();
    481         menuManager.setRemoveAllWhenShown(true);
    482         menuManager.addMenuListener(new IMenuListener() {
    483             /**
    484              * The menu is about to be shown. The menu manager has already been
    485              * requested to remove any existing menu item. This method gets the
    486              * tree selection and if it is of the appropriate type it re-creates
    487              * the necessary actions.
    488              */
    489            public void menuAboutToShow(IMenuManager manager) {
    490                ISelection selection = mTreeViewer.getSelection();
    491                if (!selection.isEmpty() && selection instanceof ITreeSelection) {
    492                    ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
    493                    doCreateMenuAction(manager, selected);
    494                    return;
    495                }
    496                doCreateMenuAction(manager, null /* ui_node */);
    497             }
    498         });
    499         Menu contextMenu = menuManager.createContextMenu(tree);
    500         tree.setMenu(contextMenu);
    501     }
    502 
    503     /**
    504      * Adds the menu actions to the context menu when the given UI node is selected in
    505      * the tree view.
    506      *
    507      * @param manager The context menu manager
    508      * @param selected The UI nodes selected in the tree. Can be null, in which case the root
    509      *                is to be modified.
    510      */
    511     private void doCreateMenuAction(IMenuManager manager, ArrayList<UiElementNode> selected) {
    512         if (selected != null) {
    513             boolean hasXml = false;
    514             for (UiElementNode uiNode : selected) {
    515                 if (uiNode.getXmlNode() != null) {
    516                     hasXml = true;
    517                     break;
    518                 }
    519             }
    520 
    521             if (hasXml) {
    522                 manager.add(new CopyCutAction(getEditor(), getClipboard(),
    523                         null, selected, true /* cut */));
    524                 manager.add(new CopyCutAction(getEditor(), getClipboard(),
    525                         null, selected, false /* cut */));
    526 
    527                 // Can't paste with more than one element selected (the selection is the target)
    528                 if (selected.size() <= 1) {
    529                     // Paste is not valid if it would add a second element on a terminal element
    530                     // which parent is a document -- an XML document can only have one child. This
    531                     // means paste is valid if the current UI node can have children or if the
    532                     // parent is not a document.
    533                     UiElementNode ui_root = selected.get(0).getUiRoot();
    534                     if (ui_root.getDescriptor().hasChildren() ||
    535                             !(ui_root.getUiParent() instanceof UiDocumentNode)) {
    536                         manager.add(new PasteAction(getEditor(), getClipboard(), selected.get(0)));
    537                     }
    538                 }
    539                 manager.add(new Separator());
    540             }
    541         }
    542 
    543         // Append "add" and "remove" actions. They do the same thing as the add/remove
    544         // buttons on the side.
    545         IconFactory factory = IconFactory.getInstance();
    546 
    547         // "Add" makes sense only if there's 0 or 1 item selected since the
    548         // one selected item becomes the target.
    549         if (selected == null || selected.size() <= 1) {
    550             manager.add(new Action("Add...", factory.getImageDescriptor("add")) { //$NON-NLS-1$
    551                 @Override
    552                 public void run() {
    553                     super.run();
    554                     doTreeAdd();
    555                 }
    556             });
    557         }
    558 
    559         if (selected != null) {
    560             if (selected != null) {
    561                 manager.add(new Action("Remove", factory.getImageDescriptor("delete")) { //$NON-NLS-1$
    562                     @Override
    563                     public void run() {
    564                         super.run();
    565                         doTreeRemove();
    566                     }
    567                 });
    568             }
    569             manager.add(new Separator());
    570 
    571             manager.add(new Action("Up", factory.getImageDescriptor("up")) { //$NON-NLS-1$
    572                 @Override
    573                 public void run() {
    574                     super.run();
    575                     doTreeUp();
    576                 }
    577             });
    578             manager.add(new Action("Down", factory.getImageDescriptor("down")) { //$NON-NLS-1$
    579                 @Override
    580                 public void run() {
    581                     super.run();
    582                     doTreeDown();
    583                 }
    584             });
    585         }
    586     }
    587 
    588 
    589     /**
    590      * This is called by the tree when a selection is made.
    591      * It enables/disables the buttons associated with the tree depending on the current
    592      * selection.
    593      *
    594      * @param selection The current tree selection (same as mTreeViewer.getSelection())
    595      */
    596     private void adjustTreeButtons(ISelection selection) {
    597         mRemoveButton.setEnabled(!selection.isEmpty() && selection instanceof ITreeSelection);
    598         mUpButton.setEnabled(canDoTreeUp(selection));
    599         mDownButton.setEnabled(canDoTreeDown(selection));
    600     }
    601 
    602     /**
    603      * An adapter/wrapper to use the add/remove/up/down tree edit actions.
    604      */
    605     private class UiTreeActions extends UiActions {
    606         @Override
    607         protected UiElementNode getRootNode() {
    608             return mUiRootNode;
    609         }
    610 
    611         @Override
    612         protected void selectUiNode(UiElementNode uiNodeToSelect) {
    613             // Select the new item
    614             if (uiNodeToSelect != null) {
    615                 LinkedList<UiElementNode> segments = new LinkedList<UiElementNode>();
    616                 for (UiElementNode ui_node = uiNodeToSelect; ui_node != mUiRootNode;
    617                         ui_node = ui_node.getUiParent()) {
    618                     segments.add(0, ui_node);
    619                 }
    620                 if (segments.size() > 0) {
    621                     mTreeViewer.setSelection(new TreeSelection(new TreePath(segments.toArray())));
    622                 } else {
    623                     mTreeViewer.setSelection(null);
    624                 }
    625             }
    626         }
    627 
    628         @Override
    629         public void commitPendingXmlChanges() {
    630             commitManagedForm();
    631         }
    632     }
    633 
    634     /**
    635      * Filters an ITreeSelection to only keep the {@link UiElementNode}s (in case there's
    636      * something else in there).
    637      *
    638      * @return A new list of {@link UiElementNode} with at least one item or null.
    639      */
    640     @SuppressWarnings("unchecked")
    641     private ArrayList<UiElementNode> filterSelection(ITreeSelection selection) {
    642         ArrayList<UiElementNode> selected = new ArrayList<UiElementNode>();
    643 
    644         for (Iterator it = selection.iterator(); it.hasNext(); ) {
    645             Object selectedObj = it.next();
    646 
    647             if (selectedObj instanceof UiElementNode) {
    648                 selected.add((UiElementNode) selectedObj);
    649             }
    650         }
    651 
    652         return selected.size() > 0 ? selected : null;
    653     }
    654 
    655     /**
    656      * Called when the "Add..." button next to the tree view is selected.
    657      *
    658      * Displays a selection dialog that lets the user select which kind of node
    659      * to create, depending on the current selection.
    660      */
    661     private void doTreeAdd() {
    662         UiElementNode ui_node = mUiRootNode;
    663         ISelection selection = mTreeViewer.getSelection();
    664         if (!selection.isEmpty() && selection instanceof ITreeSelection) {
    665             ITreeSelection tree_selection = (ITreeSelection) selection;
    666             Object first = tree_selection.getFirstElement();
    667             if (first != null && first instanceof UiElementNode) {
    668                 ui_node = (UiElementNode) first;
    669             }
    670         }
    671 
    672         mUiTreeActions.doAdd(
    673                 ui_node,
    674                 mDescriptorFilters,
    675                 mTreeViewer.getControl().getShell(),
    676                 (ILabelProvider) mTreeViewer.getLabelProvider());
    677     }
    678 
    679     /**
    680      * Called when the "Remove" button is selected.
    681      *
    682      * If the tree has a selection, remove it.
    683      * This simply deletes the XML node attached to the UI node: when the XML model fires the
    684      * update event, the tree will get refreshed.
    685      */
    686     protected void doTreeRemove() {
    687         ISelection selection = mTreeViewer.getSelection();
    688         if (!selection.isEmpty() && selection instanceof ITreeSelection) {
    689             ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
    690             mUiTreeActions.doRemove(selected, mTreeViewer.getControl().getShell());
    691         }
    692     }
    693 
    694     /**
    695      * Called when the "Up" button is selected.
    696      * <p/>
    697      * If the tree has a selection, move it up, either in the child list or as the last child
    698      * of the previous parent.
    699      */
    700     protected void doTreeUp() {
    701         ISelection selection = mTreeViewer.getSelection();
    702         if (!selection.isEmpty() && selection instanceof ITreeSelection) {
    703             ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
    704             mUiTreeActions.doUp(selected, mDescriptorFilters);
    705         }
    706     }
    707 
    708     /**
    709      * Checks whether the "up" action can be done on the current selection.
    710      *
    711      * @param selection The current tree selection.
    712      * @return True if all the selected nodes can be moved up.
    713      */
    714     protected boolean canDoTreeUp(ISelection selection) {
    715         if (!selection.isEmpty() && selection instanceof ITreeSelection) {
    716             ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
    717             return mUiTreeActions.canDoUp(selected, mDescriptorFilters);
    718         }
    719 
    720         return false;
    721     }
    722 
    723     /**
    724      * Called when the "Down" button is selected.
    725      *
    726      * If the tree has a selection, move it down, either in the same child list or as the
    727      * first child of the next parent.
    728      */
    729     protected void doTreeDown() {
    730         ISelection selection = mTreeViewer.getSelection();
    731         if (!selection.isEmpty() && selection instanceof ITreeSelection) {
    732             ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
    733             mUiTreeActions.doDown(selected, mDescriptorFilters);
    734         }
    735     }
    736 
    737     /**
    738      * Checks whether the "down" action can be done on the current selection.
    739      *
    740      * @param selection The current tree selection.
    741      * @return True if all the selected nodes can be moved down.
    742      */
    743     protected boolean canDoTreeDown(ISelection selection) {
    744         if (!selection.isEmpty() && selection instanceof ITreeSelection) {
    745             ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
    746             return mUiTreeActions.canDoDown(selected, mDescriptorFilters);
    747         }
    748 
    749         return false;
    750     }
    751 
    752     /**
    753      * Commits the current managed form (the one associated with our master part).
    754      * As a side effect, this will commit the current UiElementDetails page.
    755      */
    756     void commitManagedForm() {
    757         if (mManagedForm != null) {
    758             mManagedForm.commit(false /* onSave */);
    759         }
    760     }
    761 
    762     /* Implements ICommitXml for CopyCutAction */
    763     public void commitPendingXmlChanges() {
    764         commitManagedForm();
    765     }
    766 
    767     @Override
    768     protected void createToolBarActions(IManagedForm managedForm) {
    769         // Pass. Not used, toolbar actions are defined by createSectionActions().
    770     }
    771 
    772     @Override
    773     protected void registerPages(DetailsPart inDetailsPart) {
    774         // Keep a reference on the details part (the super class doesn't provide a getter
    775         // for it.)
    776         mDetailsPart = inDetailsPart;
    777 
    778         // The page selection mechanism does not use pages registered by association with
    779         // a node class. Instead it uses a custom details page provider that provides a
    780         // new UiElementDetail instance for each node instance. A limit of 5 pages is
    781         // then set (the value is arbitrary but should be reasonable) for the internal
    782         // page book.
    783         inDetailsPart.setPageLimit(5);
    784 
    785         final UiTreeBlock tree = this;
    786 
    787         inDetailsPart.setPageProvider(new IDetailsPageProvider() {
    788             public IDetailsPage getPage(Object key) {
    789                 if (key instanceof UiElementNode) {
    790                     return new UiElementDetail(tree);
    791                 }
    792                 return null;
    793             }
    794 
    795             public Object getPageKey(Object object) {
    796                 return object;  // use node object as key
    797             }
    798         });
    799     }
    800 
    801     /**
    802      * An alphabetic sort action for the tree viewer.
    803      */
    804     private class TreeSortAction extends Action {
    805 
    806         private ViewerComparator mComparator;
    807 
    808         public TreeSortAction() {
    809             super("Sorts elements alphabetically.", AS_CHECK_BOX);
    810             setImageDescriptor(IconFactory.getInstance().getImageDescriptor("az_sort")); //$NON-NLS-1$
    811 
    812             if (mTreeViewer != null) {
    813                 boolean is_sorted = mTreeViewer.getComparator() != null;
    814                 setChecked(is_sorted);
    815             }
    816         }
    817 
    818         /**
    819          * Called when the button is selected. Toggles the tree viewer comparator.
    820          */
    821         @Override
    822         public void run() {
    823             if (mTreeViewer == null) {
    824                 notifyResult(false /*success*/);
    825                 return;
    826             }
    827 
    828             ViewerComparator comp = mTreeViewer.getComparator();
    829             if (comp != null) {
    830                 // Tree is currently sorted.
    831                 // Save currently comparator and remove it
    832                 mComparator = comp;
    833                 mTreeViewer.setComparator(null);
    834             } else {
    835                 // Tree is not currently sorted.
    836                 // Reuse or add a new comparator.
    837                 if (mComparator == null) {
    838                     mComparator = new ViewerComparator();
    839                 }
    840                 mTreeViewer.setComparator(mComparator);
    841             }
    842 
    843             notifyResult(true /*success*/);
    844         }
    845     }
    846 
    847     /**
    848      * A filter on descriptor for the tree viewer.
    849      * <p/>
    850      * The tree viewer will contain many of these actions and only one can be enabled at a
    851      * given time. When no action is selected, everything is displayed.
    852      * <p/>
    853      * Since "radio"-like actions do not allow for unselecting all of them, we manually
    854      * handle the exclusive radio button-like property: when an action is selected, it manually
    855      * removes all other actions as needed.
    856      */
    857     private class DescriptorFilterAction extends Action {
    858 
    859         private final ElementDescriptor mDescriptor;
    860         private ViewerFilter mFilter;
    861 
    862         public DescriptorFilterAction(ElementDescriptor descriptor) {
    863             super(String.format("Displays only %1$s elements.", descriptor.getUiName()),
    864                     AS_CHECK_BOX);
    865 
    866             mDescriptor = descriptor;
    867             setImageDescriptor(descriptor.getImageDescriptor());
    868         }
    869 
    870         /**
    871          * Called when the button is selected.
    872          * <p/>
    873          * Find any existing {@link DescriptorFilter}s and remove them. Install ours.
    874          */
    875         @Override
    876         public void run() {
    877             super.run();
    878 
    879             if (isChecked()) {
    880                 if (mFilter == null) {
    881                     // create filter when required
    882                     mFilter = new DescriptorFilter(this);
    883                 }
    884 
    885                 // we add our filter first, otherwise the UI might show the full list
    886                 mTreeViewer.addFilter(mFilter);
    887 
    888                 // Then remove the any other filters except ours. There should be at most
    889                 // one other filter, since that's how the actions are made to look like
    890                 // exclusive radio buttons.
    891                 for (ViewerFilter filter : mTreeViewer.getFilters()) {
    892                     if (filter instanceof DescriptorFilter && filter != mFilter) {
    893                         DescriptorFilterAction action = ((DescriptorFilter) filter).getAction();
    894                         action.setChecked(false);
    895                         mTreeViewer.removeFilter(filter);
    896                     }
    897                 }
    898             } else if (mFilter != null){
    899                 mTreeViewer.removeFilter(mFilter);
    900             }
    901         }
    902 
    903         /**
    904          * Filters the tree viewer for the given descriptor.
    905          * <p/>
    906          * The filter is linked to the action so that an action can iterate through the list
    907          * of filters and un-select the actions.
    908          */
    909         private class DescriptorFilter extends ViewerFilter {
    910 
    911             private final DescriptorFilterAction mAction;
    912 
    913             public DescriptorFilter(DescriptorFilterAction action) {
    914                 mAction = action;
    915             }
    916 
    917             public DescriptorFilterAction getAction() {
    918                 return mAction;
    919             }
    920 
    921             /**
    922              * Returns true if an element should be displayed, that if the element or
    923              * any of its parent matches the requested descriptor.
    924              */
    925             @Override
    926             public boolean select(Viewer viewer, Object parentElement, Object element) {
    927                 while (element instanceof UiElementNode) {
    928                     UiElementNode uiNode = (UiElementNode)element;
    929                     if (uiNode.getDescriptor() == mDescriptor) {
    930                         return true;
    931                     }
    932                     element = uiNode.getUiParent();
    933                 }
    934                 return false;
    935             }
    936         }
    937     }
    938 
    939 }
    940