• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
UiTreeBlock(AndroidXmlEditor editor, UiElementNode uiRootNode, boolean autoCreateRoot, ElementDescriptor[] descriptorFilters, String title, String description)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 */
getEditor()172     AndroidXmlEditor getEditor() {
173         return mEditor;
174     }
175 
176     /** @returns The reference to the clipboard for copy-paste */
getClipboard()177     Clipboard getClipboard() {
178         return mClipboard;
179     }
180 
181     /** @returns The master-detail part, composed of a main tree and an auxiliary detail part */
getMasterPart()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      */
getRootNode()193     public UiElementNode getRootNode() {
194         return mUiRootNode;
195     }
196 
197     @Override
createMasterPart(final IManagedForm managedForm, Composite parent)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 
createSectionActions(Section section, FormToolkit toolkit)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      */
createTreeViewer(FormToolkit toolkit, Composite grid, final IManagedForm managedForm)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      */
changeRootAndDescriptors(UiElementNode uiRootNode, ElementDescriptor[] descriptorFilters, boolean forceRefresh)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      */
createButtons(FormToolkit toolkit, Composite grid)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 
createTreeContextMenu(Tree tree)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      */
doCreateMenuAction(IMenuManager manager, ArrayList<UiElementNode> selected)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      */
adjustTreeButtons(ISelection selection)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
getRootNode()612         protected UiElementNode getRootNode() {
613             return mUiRootNode;
614         }
615 
616         @Override
selectUiNode(UiElementNode uiNodeToSelect)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
commitPendingXmlChanges()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      */
filterSelection(ITreeSelection selection)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      */
doTreeAdd()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      */
doTreeRemove()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      */
doTreeUp()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      */
canDoTreeUp(ISelection selection)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      */
doTreeDown()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      */
canDoTreeDown(ISelection selection)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      */
commitManagedForm()760     void commitManagedForm() {
761         if (mManagedForm != null) {
762             mManagedForm.commit(false /* onSave */);
763         }
764     }
765 
766     /* Implements ICommitXml for CopyCutAction */
767     @Override
commitPendingXmlChanges()768     public void commitPendingXmlChanges() {
769         commitManagedForm();
770     }
771 
772     @Override
createToolBarActions(IManagedForm managedForm)773     protected void createToolBarActions(IManagedForm managedForm) {
774         // Pass. Not used, toolbar actions are defined by createSectionActions().
775     }
776 
777     @Override
registerPages(DetailsPart inDetailsPart)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 
TreeSortAction()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
run()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 
DescriptorFilterAction(ElementDescriptor descriptor)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
run()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 
DescriptorFilter(DescriptorFilterAction action)920             public DescriptorFilter(DescriptorFilterAction action) {
921                 mAction = action;
922             }
923 
getAction()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
select(Viewer viewer, Object parentElement, Object element)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