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             @Override
    265             public void selectionChanged(SelectionChangedEvent event) {
    266                 managedForm.fireSelectionChanged(mMasterPart, event.getSelection());
    267                 adjustTreeButtons(event.getSelection());
    268             }
    269         });
    270 
    271         // Create three listeners:
    272         // - One to refresh the tree viewer when the parent's node has been updated
    273         // - One to refresh the tree viewer when the framework resources have changed
    274         // - One to enable/disable the UI based on the application node's presence.
    275         mUiRefreshListener = new IUiUpdateListener() {
    276             @Override
    277             public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) {
    278                 mTreeViewer.refresh();
    279             }
    280         };
    281 
    282         mUiEnableListener = new IUiUpdateListener() {
    283             @Override
    284             public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) {
    285                 // The UiElementNode for the application XML node always exists, even
    286                 // if there is no corresponding XML node in the XML file.
    287                 //
    288                 // Normally, we enable the UI here if the XML node is not null.
    289                 //
    290                 // However if mAutoCreateRoot is true, the root node will be created on-demand
    291                 // so the tree/block is always enabled.
    292                 boolean exists = mAutoCreateRoot || (ui_node.getXmlNode() != null);
    293                 if (mMasterPart != null) {
    294                     Section section = mMasterPart.getSection();
    295                     if (section.getEnabled() != exists) {
    296                         section.setEnabled(exists);
    297                         for (Control c : section.getChildren()) {
    298                             c.setEnabled(exists);
    299                         }
    300                     }
    301                 }
    302             }
    303         };
    304 
    305         /** Listener to update the root node if the target of the file is changed because of a
    306          * SDK location change or a project target change */
    307         final ITargetChangeListener targetListener = new TargetChangeListener() {
    308             @Override
    309             public IProject getProject() {
    310                 if (mEditor != null) {
    311                     return mEditor.getProject();
    312                 }
    313 
    314                 return null;
    315             }
    316 
    317             @Override
    318             public void reload() {
    319                 // If a details part has been created, we need to "refresh" it too.
    320                 if (mDetailsPart != null) {
    321                     // The details part does not directly expose access to its internal
    322                     // page book. Instead it is possible to resize the page book to 0 and then
    323                     // back to its original value, which has the side effect of removing all
    324                     // existing cached pages.
    325                     int limit = mDetailsPart.getPageLimit();
    326                     mDetailsPart.setPageLimit(0);
    327                     mDetailsPart.setPageLimit(limit);
    328                 }
    329                 // Refresh the tree, preserving the selection if possible.
    330                 mTreeViewer.refresh();
    331             }
    332         };
    333 
    334         // Setup the listeners
    335         changeRootAndDescriptors(mUiRootNode, mDescriptorFilters, false /* refresh */);
    336 
    337         // Listen on resource framework changes to refresh the tree
    338         AdtPlugin.getDefault().addTargetListener(targetListener);
    339 
    340         // Remove listeners when the tree widget gets disposed.
    341         tree.addDisposeListener(new DisposeListener() {
    342             @Override
    343             public void widgetDisposed(DisposeEvent e) {
    344                 if (mUiRootNode != null) {
    345                     UiElementNode node = mUiRootNode.getUiParent() != null ?
    346                                             mUiRootNode.getUiParent() :
    347                                             mUiRootNode;
    348 
    349                     if (node != null) {
    350                         node.removeUpdateListener(mUiRefreshListener);
    351                     }
    352                     mUiRootNode.removeUpdateListener(mUiEnableListener);
    353                 }
    354 
    355                 AdtPlugin.getDefault().removeTargetListener(targetListener);
    356                 if (mClipboard != null) {
    357                     mClipboard.dispose();
    358                     mClipboard = null;
    359                 }
    360             }
    361         });
    362 
    363         // Get a new clipboard reference. It is disposed when the tree is disposed.
    364         mClipboard = new Clipboard(tree.getDisplay());
    365 
    366         return tree;
    367     }
    368 
    369     /**
    370      * Changes the UI root node and the descriptor filters of the tree.
    371      * <p/>
    372      * This removes the listeners attached to the old root node and reattaches them to the
    373      * new one.
    374      *
    375      * @param uiRootNode The root {@link UiElementNode} which contains all the elements that are
    376      *        to be manipulated by this tree view. In general this is the manifest UI node or the
    377      *        application UI node. This cannot be null.
    378      * @param descriptorFilters A list of descriptors of the elements to be displayed as root in
    379      *        this tree view. Use null or an empty list to accept any kind of node.
    380      * @param forceRefresh If tree, forces the tree to refresh
    381      */
    382     public void changeRootAndDescriptors(UiElementNode uiRootNode,
    383             ElementDescriptor[] descriptorFilters, boolean forceRefresh) {
    384         UiElementNode node;
    385 
    386         // Remove previous listeners if any
    387         if (mUiRootNode != null) {
    388             node = mUiRootNode.getUiParent() != null ? mUiRootNode.getUiParent() : mUiRootNode;
    389             node.removeUpdateListener(mUiRefreshListener);
    390             mUiRootNode.removeUpdateListener(mUiEnableListener);
    391         }
    392 
    393         mUiRootNode = uiRootNode;
    394         mDescriptorFilters = descriptorFilters;
    395 
    396         mTreeViewer.setContentProvider(
    397                 new UiModelTreeContentProvider(mUiRootNode, mDescriptorFilters));
    398 
    399         // Listen on structural changes on the root node of the tree
    400         // If the node has a parent, listen on the parent instead.
    401         if (mUiRootNode != null) {
    402             node = mUiRootNode.getUiParent() != null ? mUiRootNode.getUiParent() : mUiRootNode;
    403 
    404             if (node != null) {
    405                 node.addUpdateListener(mUiRefreshListener);
    406             }
    407 
    408             // Use the root node to listen to its presence.
    409             mUiRootNode.addUpdateListener(mUiEnableListener);
    410 
    411             // Initialize the enabled/disabled state
    412             mUiEnableListener.uiElementNodeUpdated(mUiRootNode, null /* state, not used */);
    413         }
    414 
    415         if (forceRefresh) {
    416             mTreeViewer.refresh();
    417         }
    418 
    419         createSectionActions(mMasterPart.getSection(), mManagedForm.getToolkit());
    420     }
    421 
    422     /**
    423      * Creates the buttons next to the tree.
    424      */
    425     private void createButtons(FormToolkit toolkit, Composite grid) {
    426 
    427         mUiTreeActions = new UiTreeActions();
    428 
    429         Composite button_grid = SectionHelper.createGridLayout(grid, toolkit, 1);
    430         button_grid.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_BEGINNING));
    431         mAddButton = toolkit.createButton(button_grid, "Add...", SWT.PUSH);
    432         SectionHelper.addControlTooltip(mAddButton, "Adds a new element.");
    433         mAddButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL |
    434                 GridData.VERTICAL_ALIGN_BEGINNING));
    435 
    436         mAddButton.addSelectionListener(new SelectionAdapter() {
    437             @Override
    438             public void widgetSelected(SelectionEvent e) {
    439                 super.widgetSelected(e);
    440                 doTreeAdd();
    441             }
    442         });
    443 
    444         mRemoveButton = toolkit.createButton(button_grid, "Remove...", SWT.PUSH);
    445         SectionHelper.addControlTooltip(mRemoveButton, "Removes an existing selected element.");
    446         mRemoveButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
    447 
    448         mRemoveButton.addSelectionListener(new SelectionAdapter() {
    449             @Override
    450             public void widgetSelected(SelectionEvent e) {
    451                 super.widgetSelected(e);
    452                 doTreeRemove();
    453             }
    454         });
    455 
    456         mUpButton = toolkit.createButton(button_grid, "Up", SWT.PUSH);
    457         SectionHelper.addControlTooltip(mRemoveButton, "Moves the selected element up.");
    458         mUpButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
    459 
    460         mUpButton.addSelectionListener(new SelectionAdapter() {
    461             @Override
    462             public void widgetSelected(SelectionEvent e) {
    463                 super.widgetSelected(e);
    464                 doTreeUp();
    465             }
    466         });
    467 
    468         mDownButton = toolkit.createButton(button_grid, "Down", SWT.PUSH);
    469         SectionHelper.addControlTooltip(mRemoveButton, "Moves the selected element down.");
    470         mDownButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
    471 
    472         mDownButton.addSelectionListener(new SelectionAdapter() {
    473             @Override
    474             public void widgetSelected(SelectionEvent e) {
    475                 super.widgetSelected(e);
    476                 doTreeDown();
    477             }
    478         });
    479 
    480         adjustTreeButtons(TreeSelection.EMPTY);
    481     }
    482 
    483     private void createTreeContextMenu(Tree tree) {
    484         MenuManager menuManager = new MenuManager();
    485         menuManager.setRemoveAllWhenShown(true);
    486         menuManager.addMenuListener(new IMenuListener() {
    487             /**
    488              * The menu is about to be shown. The menu manager has already been
    489              * requested to remove any existing menu item. This method gets the
    490              * tree selection and if it is of the appropriate type it re-creates
    491              * the necessary actions.
    492              */
    493            @Override
    494         public void menuAboutToShow(IMenuManager manager) {
    495                ISelection selection = mTreeViewer.getSelection();
    496                if (!selection.isEmpty() && selection instanceof ITreeSelection) {
    497                    ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
    498                    doCreateMenuAction(manager, selected);
    499                    return;
    500                }
    501                doCreateMenuAction(manager, null /* ui_node */);
    502             }
    503         });
    504         Menu contextMenu = menuManager.createContextMenu(tree);
    505         tree.setMenu(contextMenu);
    506     }
    507 
    508     /**
    509      * Adds the menu actions to the context menu when the given UI node is selected in
    510      * the tree view.
    511      *
    512      * @param manager The context menu manager
    513      * @param selected The UI nodes selected in the tree. Can be null, in which case the root
    514      *                is to be modified.
    515      */
    516     private void doCreateMenuAction(IMenuManager manager, ArrayList<UiElementNode> selected) {
    517         if (selected != null) {
    518             boolean hasXml = false;
    519             for (UiElementNode uiNode : selected) {
    520                 if (uiNode.getXmlNode() != null) {
    521                     hasXml = true;
    522                     break;
    523                 }
    524             }
    525 
    526             if (hasXml) {
    527                 manager.add(new CopyCutAction(getEditor(), getClipboard(),
    528                         null, selected, true /* cut */));
    529                 manager.add(new CopyCutAction(getEditor(), getClipboard(),
    530                         null, selected, false /* cut */));
    531 
    532                 // Can't paste with more than one element selected (the selection is the target)
    533                 if (selected.size() <= 1) {
    534                     // Paste is not valid if it would add a second element on a terminal element
    535                     // which parent is a document -- an XML document can only have one child. This
    536                     // means paste is valid if the current UI node can have children or if the
    537                     // parent is not a document.
    538                     UiElementNode ui_root = selected.get(0).getUiRoot();
    539                     if (ui_root.getDescriptor().hasChildren() ||
    540                             !(ui_root.getUiParent() instanceof UiDocumentNode)) {
    541                         manager.add(new PasteAction(getEditor(), getClipboard(), selected.get(0)));
    542                     }
    543                 }
    544                 manager.add(new Separator());
    545             }
    546         }
    547 
    548         // Append "add" and "remove" actions. They do the same thing as the add/remove
    549         // buttons on the side.
    550         IconFactory factory = IconFactory.getInstance();
    551 
    552         // "Add" makes sense only if there's 0 or 1 item selected since the
    553         // one selected item becomes the target.
    554         if (selected == null || selected.size() <= 1) {
    555             manager.add(new Action("Add...", factory.getImageDescriptor("add")) { //$NON-NLS-1$
    556                 @Override
    557                 public void run() {
    558                     super.run();
    559                     doTreeAdd();
    560                 }
    561             });
    562         }
    563 
    564         if (selected != null) {
    565             if (selected != null) {
    566                 manager.add(new Action("Remove", factory.getImageDescriptor("delete")) { //$NON-NLS-1$
    567                     @Override
    568                     public void run() {
    569                         super.run();
    570                         doTreeRemove();
    571                     }
    572                 });
    573             }
    574             manager.add(new Separator());
    575 
    576             manager.add(new Action("Up", factory.getImageDescriptor("up")) { //$NON-NLS-1$
    577                 @Override
    578                 public void run() {
    579                     super.run();
    580                     doTreeUp();
    581                 }
    582             });
    583             manager.add(new Action("Down", factory.getImageDescriptor("down")) { //$NON-NLS-1$
    584                 @Override
    585                 public void run() {
    586                     super.run();
    587                     doTreeDown();
    588                 }
    589             });
    590         }
    591     }
    592 
    593 
    594     /**
    595      * This is called by the tree when a selection is made.
    596      * It enables/disables the buttons associated with the tree depending on the current
    597      * selection.
    598      *
    599      * @param selection The current tree selection (same as mTreeViewer.getSelection())
    600      */
    601     private void adjustTreeButtons(ISelection selection) {
    602         mRemoveButton.setEnabled(!selection.isEmpty() && selection instanceof ITreeSelection);
    603         mUpButton.setEnabled(canDoTreeUp(selection));
    604         mDownButton.setEnabled(canDoTreeDown(selection));
    605     }
    606 
    607     /**
    608      * An adapter/wrapper to use the add/remove/up/down tree edit actions.
    609      */
    610     private class UiTreeActions extends UiActions {
    611         @Override
    612         protected UiElementNode getRootNode() {
    613             return mUiRootNode;
    614         }
    615 
    616         @Override
    617         protected void selectUiNode(UiElementNode uiNodeToSelect) {
    618             // Select the new item
    619             if (uiNodeToSelect != null) {
    620                 LinkedList<UiElementNode> segments = new LinkedList<UiElementNode>();
    621                 for (UiElementNode ui_node = uiNodeToSelect; ui_node != mUiRootNode;
    622                         ui_node = ui_node.getUiParent()) {
    623                     segments.add(0, ui_node);
    624                 }
    625                 if (segments.size() > 0) {
    626                     mTreeViewer.setSelection(new TreeSelection(new TreePath(segments.toArray())));
    627                 } else {
    628                     mTreeViewer.setSelection(null);
    629                 }
    630             }
    631         }
    632 
    633         @Override
    634         public void commitPendingXmlChanges() {
    635             commitManagedForm();
    636         }
    637     }
    638 
    639     /**
    640      * Filters an ITreeSelection to only keep the {@link UiElementNode}s (in case there's
    641      * something else in there).
    642      *
    643      * @return A new list of {@link UiElementNode} with at least one item or null.
    644      */
    645     private ArrayList<UiElementNode> filterSelection(ITreeSelection selection) {
    646         ArrayList<UiElementNode> selected = new ArrayList<UiElementNode>();
    647 
    648         for (Iterator<Object> it = selection.iterator(); it.hasNext(); ) {
    649             Object selectedObj = it.next();
    650 
    651             if (selectedObj instanceof UiElementNode) {
    652                 selected.add((UiElementNode) selectedObj);
    653             }
    654         }
    655 
    656         return selected.size() > 0 ? selected : null;
    657     }
    658 
    659     /**
    660      * Called when the "Add..." button next to the tree view is selected.
    661      *
    662      * Displays a selection dialog that lets the user select which kind of node
    663      * to create, depending on the current selection.
    664      */
    665     private void doTreeAdd() {
    666         UiElementNode ui_node = mUiRootNode;
    667         ISelection selection = mTreeViewer.getSelection();
    668         if (!selection.isEmpty() && selection instanceof ITreeSelection) {
    669             ITreeSelection tree_selection = (ITreeSelection) selection;
    670             Object first = tree_selection.getFirstElement();
    671             if (first != null && first instanceof UiElementNode) {
    672                 ui_node = (UiElementNode) first;
    673             }
    674         }
    675 
    676         mUiTreeActions.doAdd(
    677                 ui_node,
    678                 mDescriptorFilters,
    679                 mTreeViewer.getControl().getShell(),
    680                 (ILabelProvider) mTreeViewer.getLabelProvider());
    681     }
    682 
    683     /**
    684      * Called when the "Remove" button is selected.
    685      *
    686      * If the tree has a selection, remove it.
    687      * This simply deletes the XML node attached to the UI node: when the XML model fires the
    688      * update event, the tree will get refreshed.
    689      */
    690     protected void doTreeRemove() {
    691         ISelection selection = mTreeViewer.getSelection();
    692         if (!selection.isEmpty() && selection instanceof ITreeSelection) {
    693             ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
    694             mUiTreeActions.doRemove(selected, mTreeViewer.getControl().getShell());
    695         }
    696     }
    697 
    698     /**
    699      * Called when the "Up" button is selected.
    700      * <p/>
    701      * If the tree has a selection, move it up, either in the child list or as the last child
    702      * of the previous parent.
    703      */
    704     protected void doTreeUp() {
    705         ISelection selection = mTreeViewer.getSelection();
    706         if (!selection.isEmpty() && selection instanceof ITreeSelection) {
    707             ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
    708             mUiTreeActions.doUp(selected, mDescriptorFilters);
    709         }
    710     }
    711 
    712     /**
    713      * Checks whether the "up" action can be done on the current selection.
    714      *
    715      * @param selection The current tree selection.
    716      * @return True if all the selected nodes can be moved up.
    717      */
    718     protected boolean canDoTreeUp(ISelection selection) {
    719         if (!selection.isEmpty() && selection instanceof ITreeSelection) {
    720             ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
    721             return mUiTreeActions.canDoUp(selected, mDescriptorFilters);
    722         }
    723 
    724         return false;
    725     }
    726 
    727     /**
    728      * Called when the "Down" button is selected.
    729      *
    730      * If the tree has a selection, move it down, either in the same child list or as the
    731      * first child of the next parent.
    732      */
    733     protected void doTreeDown() {
    734         ISelection selection = mTreeViewer.getSelection();
    735         if (!selection.isEmpty() && selection instanceof ITreeSelection) {
    736             ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
    737             mUiTreeActions.doDown(selected, mDescriptorFilters);
    738         }
    739     }
    740 
    741     /**
    742      * Checks whether the "down" action can be done on the current selection.
    743      *
    744      * @param selection The current tree selection.
    745      * @return True if all the selected nodes can be moved down.
    746      */
    747     protected boolean canDoTreeDown(ISelection selection) {
    748         if (!selection.isEmpty() && selection instanceof ITreeSelection) {
    749             ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
    750             return mUiTreeActions.canDoDown(selected, mDescriptorFilters);
    751         }
    752 
    753         return false;
    754     }
    755 
    756     /**
    757      * Commits the current managed form (the one associated with our master part).
    758      * As a side effect, this will commit the current UiElementDetails page.
    759      */
    760     void commitManagedForm() {
    761         if (mManagedForm != null) {
    762             mManagedForm.commit(false /* onSave */);
    763         }
    764     }
    765 
    766     /* Implements ICommitXml for CopyCutAction */
    767     @Override
    768     public void commitPendingXmlChanges() {
    769         commitManagedForm();
    770     }
    771 
    772     @Override
    773     protected void createToolBarActions(IManagedForm managedForm) {
    774         // Pass. Not used, toolbar actions are defined by createSectionActions().
    775     }
    776 
    777     @Override
    778     protected void registerPages(DetailsPart inDetailsPart) {
    779         // Keep a reference on the details part (the super class doesn't provide a getter
    780         // for it.)
    781         mDetailsPart = inDetailsPart;
    782 
    783         // The page selection mechanism does not use pages registered by association with
    784         // a node class. Instead it uses a custom details page provider that provides a
    785         // new UiElementDetail instance for each node instance. A limit of 5 pages is
    786         // then set (the value is arbitrary but should be reasonable) for the internal
    787         // page book.
    788         inDetailsPart.setPageLimit(5);
    789 
    790         final UiTreeBlock tree = this;
    791 
    792         inDetailsPart.setPageProvider(new IDetailsPageProvider() {
    793             @Override
    794             public IDetailsPage getPage(Object key) {
    795                 if (key instanceof UiElementNode) {
    796                     return new UiElementDetail(tree);
    797                 }
    798                 return null;
    799             }
    800 
    801             @Override
    802             public Object getPageKey(Object object) {
    803                 return object;  // use node object as key
    804             }
    805         });
    806     }
    807 
    808     /**
    809      * An alphabetic sort action for the tree viewer.
    810      */
    811     private class TreeSortAction extends Action {
    812 
    813         private ViewerComparator mComparator;
    814 
    815         public TreeSortAction() {
    816             super("Sorts elements alphabetically.", AS_CHECK_BOX);
    817             setImageDescriptor(IconFactory.getInstance().getImageDescriptor("az_sort")); //$NON-NLS-1$
    818 
    819             if (mTreeViewer != null) {
    820                 boolean is_sorted = mTreeViewer.getComparator() != null;
    821                 setChecked(is_sorted);
    822             }
    823         }
    824 
    825         /**
    826          * Called when the button is selected. Toggles the tree viewer comparator.
    827          */
    828         @Override
    829         public void run() {
    830             if (mTreeViewer == null) {
    831                 notifyResult(false /*success*/);
    832                 return;
    833             }
    834 
    835             ViewerComparator comp = mTreeViewer.getComparator();
    836             if (comp != null) {
    837                 // Tree is currently sorted.
    838                 // Save currently comparator and remove it
    839                 mComparator = comp;
    840                 mTreeViewer.setComparator(null);
    841             } else {
    842                 // Tree is not currently sorted.
    843                 // Reuse or add a new comparator.
    844                 if (mComparator == null) {
    845                     mComparator = new ViewerComparator();
    846                 }
    847                 mTreeViewer.setComparator(mComparator);
    848             }
    849 
    850             notifyResult(true /*success*/);
    851         }
    852     }
    853 
    854     /**
    855      * A filter on descriptor for the tree viewer.
    856      * <p/>
    857      * The tree viewer will contain many of these actions and only one can be enabled at a
    858      * given time. When no action is selected, everything is displayed.
    859      * <p/>
    860      * Since "radio"-like actions do not allow for unselecting all of them, we manually
    861      * handle the exclusive radio button-like property: when an action is selected, it manually
    862      * removes all other actions as needed.
    863      */
    864     private class DescriptorFilterAction extends Action {
    865 
    866         private final ElementDescriptor mDescriptor;
    867         private ViewerFilter mFilter;
    868 
    869         public DescriptorFilterAction(ElementDescriptor descriptor) {
    870             super(String.format("Displays only %1$s elements.", descriptor.getUiName()),
    871                     AS_CHECK_BOX);
    872 
    873             mDescriptor = descriptor;
    874             setImageDescriptor(descriptor.getImageDescriptor());
    875         }
    876 
    877         /**
    878          * Called when the button is selected.
    879          * <p/>
    880          * Find any existing {@link DescriptorFilter}s and remove them. Install ours.
    881          */
    882         @Override
    883         public void run() {
    884             super.run();
    885 
    886             if (isChecked()) {
    887                 if (mFilter == null) {
    888                     // create filter when required
    889                     mFilter = new DescriptorFilter(this);
    890                 }
    891 
    892                 // we add our filter first, otherwise the UI might show the full list
    893                 mTreeViewer.addFilter(mFilter);
    894 
    895                 // Then remove the any other filters except ours. There should be at most
    896                 // one other filter, since that's how the actions are made to look like
    897                 // exclusive radio buttons.
    898                 for (ViewerFilter filter : mTreeViewer.getFilters()) {
    899                     if (filter instanceof DescriptorFilter && filter != mFilter) {
    900                         DescriptorFilterAction action = ((DescriptorFilter) filter).getAction();
    901                         action.setChecked(false);
    902                         mTreeViewer.removeFilter(filter);
    903                     }
    904                 }
    905             } else if (mFilter != null){
    906                 mTreeViewer.removeFilter(mFilter);
    907             }
    908         }
    909 
    910         /**
    911          * Filters the tree viewer for the given descriptor.
    912          * <p/>
    913          * The filter is linked to the action so that an action can iterate through the list
    914          * of filters and un-select the actions.
    915          */
    916         private class DescriptorFilter extends ViewerFilter {
    917 
    918             private final DescriptorFilterAction mAction;
    919 
    920             public DescriptorFilter(DescriptorFilterAction action) {
    921                 mAction = action;
    922             }
    923 
    924             public DescriptorFilterAction getAction() {
    925                 return mAction;
    926             }
    927 
    928             /**
    929              * Returns true if an element should be displayed, that if the element or
    930              * any of its parent matches the requested descriptor.
    931              */
    932             @Override
    933             public boolean select(Viewer viewer, Object parentElement, Object element) {
    934                 while (element instanceof UiElementNode) {
    935                     UiElementNode uiNode = (UiElementNode)element;
    936                     if (uiNode.getDescriptor() == mDescriptor) {
    937                         return true;
    938                     }
    939                     element = uiNode.getUiParent();
    940                 }
    941                 return false;
    942             }
    943         }
    944     }
    945 
    946 }
    947