RuleAction.java revision 85e4a1a9dd133abb879ec211ce8dd385004edf22
1/*
2 * Copyright (C) 2010 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
17package com.android.ide.common.api;
18
19import com.android.annotations.NonNull;
20import com.android.annotations.Nullable;
21import com.android.utils.Pair;
22import com.google.common.annotations.Beta;
23
24import java.net.URL;
25import java.util.ArrayList;
26import java.util.List;
27import java.util.regex.Pattern;
28
29/**
30 * A {@link RuleAction} represents an action provided by an {@link IViewRule}, typically
31 * shown in a context menu or in the layout actions bar.
32 * <p/>
33 * Each action should have a reasonably unique ID. This is used when multiple nodes
34 * are selected to filter the actions down to just those actions that are supported
35 * across all selected nodes. If an action does not support multiple nodes, it can
36 * return false from {@link #supportsMultipleNodes()}.
37 * <p/>
38 * Actions can be grouped into a hierarchy of sub-menus using the {@link NestedAction} class,
39 * or into a flat submenu using the {@link Choices} class.
40 * <p/>
41 * Actions (including separators) all have a "sort priority", and this is used to
42 * sort the menu items or toolbar buttons into a specific order.
43 * <p>
44 * <b>NOTE: This is not a public or final API; if you rely on this be prepared
45 * to adjust your code for the next tools release.</b>
46 * </p>
47 */
48@Beta
49public class RuleAction implements Comparable<RuleAction> {
50    /**
51     * Character used to split multiple checked choices.
52     * The pipe character "|" is used, to natively match Android resource flag separators.
53     */
54    public final static String CHOICE_SEP = "|"; //$NON-NLS-1$
55
56    /**
57     * Same as {@link #CHOICE_SEP} but safe for use in regular expressions.
58     */
59    public final static String CHOICE_SEP_PATTERN = Pattern.quote(CHOICE_SEP);
60
61    /**
62     * The unique id of the action.
63     * @see #getId()
64     */
65    private final String mId;
66    /**
67     * The UI-visible title of the action.
68     */
69    private final String mTitle;
70
71    /** A URL pointing to an icon, or null */
72    private URL mIconUrl;
73
74    /**
75     * A callback executed when the action is selected in the context menu.
76     */
77    private final IMenuCallback mCallback;
78
79    /**
80     * The sorting priority of this item; actions can be sorted according to these
81     */
82    protected final int mSortPriority;
83
84    /**
85     * Whether this action supports multiple nodes, see
86     * {@link #supportsMultipleNodes()} for details.
87     */
88    private final boolean mSupportsMultipleNodes;
89
90    /**
91     * Special value which will insert a separator in the choices' submenu.
92     */
93    public final static String SEPARATOR = "----";
94
95    // Factories
96
97    /**
98     * Constructs a new separator which will be shown in places where separators
99     * are supported such as context menus
100     *
101     * @param sortPriority a priority used for sorting this action
102     * @return a new separator
103     */
104    @NonNull
105    public static Separator createSeparator(int sortPriority) {
106        return new Separator(sortPriority, true /* supportsMultipleNodes*/);
107    }
108
109    /**
110     * Constructs a new base {@link RuleAction} with its ID, title and action callback.
111     *
112     * @param id The unique ID of the action. Must not be null.
113     * @param title The title of the action. Must not be null.
114     * @param callback The callback executed when the action is selected.
115     *            Must not be null.
116     * @param iconUrl a URL pointing to an icon to use for this action, or null
117     * @param sortPriority a priority used for sorting this action
118     * @param supportsMultipleNodes whether this action supports multiple nodes,
119     *            see {@link #supportsMultipleNodes()} for details
120     * @return the new {@link RuleAction}
121     */
122    @NonNull
123    public static RuleAction createAction(
124            @NonNull String id,
125            @NonNull String title,
126            @NonNull IMenuCallback callback,
127            @Nullable URL iconUrl,
128            int sortPriority,
129            boolean supportsMultipleNodes) {
130        RuleAction action = new RuleAction(id, title, callback, sortPriority,
131                supportsMultipleNodes);
132        action.setIconUrl(iconUrl);
133
134        return action;
135    }
136
137    /**
138     * Creates a new immutable toggle action.
139     *
140     * @param id The unique id of the action. Cannot be null.
141     * @param title The UI-visible title of the context menu item. Cannot be null.
142     * @param isChecked Whether the context menu item has a check mark.
143     * @param callback A callback to execute when the context menu item is
144     *            selected.
145     * @param iconUrl a URL pointing to an icon to use for this action, or null
146     * @param sortPriority a priority used for sorting this action
147     * @param supportsMultipleNodes whether this action supports multiple nodes,
148     *            see {@link #supportsMultipleNodes()} for details
149     * @return the new {@link Toggle}
150     */
151    @NonNull
152    public static Toggle createToggle(
153            @NonNull String id,
154            @NonNull String title,
155            boolean isChecked,
156            @NonNull IMenuCallback callback,
157            @Nullable URL iconUrl,
158            int sortPriority,
159            boolean supportsMultipleNodes) {
160        Toggle toggle = new Toggle(id, title, isChecked, callback, sortPriority,
161                supportsMultipleNodes);
162        toggle.setIconUrl(iconUrl);
163        return toggle;
164    }
165
166    /**
167     * Creates a new immutable multiple-choice action with a defined ordered set
168     * of action children.
169     *
170     * @param id The unique id of the action. Cannot be null.
171     * @param title The title of the action to be displayed to the user
172     * @param provider Provides the actions to be shown as children of this
173     *            action
174     * @param callback A callback to execute when the context menu item is
175     *            selected.
176     * @param iconUrl the icon to use for the multiple choice action itself
177     * @param sortPriority the sorting priority to use for the multiple choice
178     *            action itself
179     * @param supportsMultipleNodes whether this action supports multiple nodes,
180     *            see {@link #supportsMultipleNodes()} for details
181     * @return the new {@link NestedAction}
182     */
183    @NonNull
184    public static NestedAction createChoices(
185            @NonNull String id,
186            @NonNull String title,
187            @NonNull IMenuCallback callback,
188            @Nullable URL iconUrl,
189            int sortPriority,
190            boolean supportsMultipleNodes,
191            @NonNull ActionProvider provider) {
192        NestedAction choices = new NestedAction(id, title, provider, callback,
193                sortPriority, supportsMultipleNodes);
194        choices.setIconUrl(iconUrl);
195        return choices;
196    }
197
198    /**
199     * Creates a new immutable multiple-choice action with a defined ordered set
200     * of children.
201     *
202     * @param id The unique id of the action. Cannot be null.
203     * @param title The title of the action to be displayed to the user
204     * @param iconUrls The icon urls for the children items (may be null)
205     * @param ids The internal ids for the children
206     * @param current The id(s) of the current choice(s) that will be check
207     *            marked. Can be null. Can be an id not present in the choices
208     *            map. There can be more than one id separated by
209     *            {@link #CHOICE_SEP}.
210     * @param callback A callback to execute when the context menu item is
211     *            selected.
212     * @param titles The UI-visible titles of the children
213     * @param iconUrl the icon to use for the multiple choice action itself
214     * @param sortPriority the sorting priority to use for the multiple choice
215     *            action itself
216     * @param supportsMultipleNodes whether this action supports multiple nodes,
217     *            see {@link #supportsMultipleNodes()} for details
218     * @return the new {@link Choices}
219     */
220    @NonNull
221    public static Choices createChoices(
222            @NonNull String id,
223            @NonNull String title,
224            @NonNull IMenuCallback callback,
225            @NonNull List<String> titles,
226            @Nullable List<URL> iconUrls,
227            @NonNull List<String> ids,
228            @Nullable String current,
229            @Nullable URL iconUrl,
230            int sortPriority,
231            boolean supportsMultipleNodes) {
232        Choices choices = new Choices(id, title, callback, titles, iconUrls,
233                ids, current, sortPriority, supportsMultipleNodes);
234        choices.setIconUrl(iconUrl);
235
236        return choices;
237    }
238
239    /**
240     * Creates a new immutable multiple-choice action with a defined ordered set
241     * of children.
242     *
243     * @param id The unique id of the action. Cannot be null.
244     * @param title The title of the action to be displayed to the user
245     * @param iconUrls The icon urls for the children items (may be null)
246     * @param current The id(s) of the current choice(s) that will be check
247     *            marked. Can be null. Can be an id not present in the choices
248     *            map. There can be more than one id separated by
249     *            {@link #CHOICE_SEP}.
250     * @param callback A callback to execute when the context menu item is
251     *            selected.
252     * @param iconUrl the icon to use for the multiple choice action itself
253     * @param sortPriority the sorting priority to use for the multiple choice
254     *            action itself
255     * @param supportsMultipleNodes whether this action supports multiple nodes,
256     *            see {@link #supportsMultipleNodes()} for details
257     * @param idsAndTitles a list of pairs (of ids and titles) to use for the
258     *            menu items
259     * @return the new {@link Choices}
260     */
261    @NonNull
262    public static Choices createChoices(
263            @NonNull String id,
264            @NonNull String title,
265            @NonNull IMenuCallback callback,
266            @Nullable List<URL> iconUrls,
267            @Nullable String current,
268            @Nullable URL iconUrl,
269            int sortPriority,
270            boolean supportsMultipleNodes,
271            @NonNull List<Pair<String, String>> idsAndTitles) {
272        int itemCount = idsAndTitles.size();
273        List<String> titles = new ArrayList<String>(itemCount);
274        List<String> ids = new ArrayList<String>(itemCount);
275        for (Pair<String, String> pair : idsAndTitles) {
276            ids.add(pair.getFirst());
277            titles.add(pair.getSecond());
278        }
279        Choices choices = new Choices(id, title, callback, titles, iconUrls,
280                ids, current, sortPriority, supportsMultipleNodes);
281        choices.setIconUrl(iconUrl);
282        return choices;
283    }
284
285    /**
286     * Creates a new immutable multiple-choice action with lazily computed children.
287     *
288     * @param id The unique id of the action. Cannot be null.
289     * @param title The title of the multiple-choice itself
290     * @param callback A callback to execute when the context menu item is
291     *            selected.
292     * @param provider the provider which provides choices lazily
293     * @param current The id(s) of the current choice(s) that will be check
294     *            marked. Can be null. Can be an id not present in the choice
295     *            alternatives. There can be more than one id separated by
296     *            {@link #CHOICE_SEP}.
297     * @param iconUrl the icon to use for the multiple choice action itself
298     * @param sortPriority the sorting priority to use for the multiple choice
299     *            action itself
300     * @param supportsMultipleNodes whether this action supports multiple nodes,
301     *            see {@link #supportsMultipleNodes()} for details
302     * @return the new {@link Choices}
303     */
304    @NonNull
305    public static Choices createChoices(
306            @NonNull String id,
307            @NonNull String title,
308            IMenuCallback callback,
309            @NonNull ChoiceProvider provider,
310            @Nullable String current,
311            @Nullable URL iconUrl,
312            int sortPriority,
313            boolean supportsMultipleNodes) {
314        Choices choices = new DelayedChoices(id, title, callback,
315                current, provider, sortPriority, supportsMultipleNodes);
316        choices.setIconUrl(iconUrl);
317        return choices;
318    }
319
320    /**
321     * Creates a new {@link RuleAction} with the given id and the given title.
322     * Actions which have the same id and the same title are deemed equivalent.
323     *
324     * @param id The unique id of the action, which must be similar for all actions that
325     *           perform the same task. Cannot be null.
326     * @param title The UI-visible title of the action.
327     * @param callback A callback to execute when the context menu item is
328     *            selected.
329     * @param sortPriority a priority used for sorting this action
330     * @param supportsMultipleNodes the new return value for
331     *            {@link #supportsMultipleNodes()}
332     */
333    private RuleAction(
334            @NonNull String id,
335            @NonNull String title,
336            @NonNull IMenuCallback callback,
337            int sortPriority,
338            boolean supportsMultipleNodes) {
339        mId = id;
340        mTitle = title;
341        mSortPriority = sortPriority;
342        mSupportsMultipleNodes = supportsMultipleNodes;
343        mCallback = callback;
344    }
345
346    /**
347     * Returns the unique id of the action. In the context of a multiple selection,
348     * actions which have the same id are collapsed together and must represent the same
349     * action. Cannot be null.
350     *
351     * @return the unique id of the action, never null
352     */
353    @NonNull
354    public String getId() {
355        return mId;
356    }
357
358    /**
359     * Returns the UI-visible title of the action, shown in the context menu.
360     * Cannot be null.
361     *
362     * @return the user name of the action, never null
363     */
364    @NonNull
365    public String getTitle() {
366        return mTitle;
367    }
368
369    /**
370     * Actions which have the same id and the same title are deemed equivalent.
371     */
372    @Override
373    public boolean equals(Object obj) {
374        if (obj instanceof RuleAction) {
375            RuleAction rhs = (RuleAction) obj;
376
377            if (mId != rhs.mId && !(mId != null && mId.equals(rhs.mId))) return false;
378            if (mTitle != rhs.mTitle &&
379                    !(mTitle != null && mTitle.equals(rhs.mTitle))) return false;
380            return true;
381        }
382        return false;
383    }
384
385    /**
386     * Whether this action supports multiple nodes. An action which supports
387     * multiple nodes can be applied to different nodes by passing in different
388     * nodes to its callback. Some actions are hardcoded for a specific node (typically
389     * one that isn't selected, such as an action which affects the parent of a selected
390     * node), and these actions will not be added to the context menu when more than
391     * one node is selected.
392     *
393     * @return true if this node supports multiple nodes
394     */
395    public boolean supportsMultipleNodes() {
396        return mSupportsMultipleNodes;
397    }
398
399    /**
400     * Actions which have the same id and the same title have the same hash code.
401     */
402    @Override
403    public int hashCode() {
404        int h = mId == null ? 0 : mId.hashCode();
405        h = h ^ (mTitle == null ? 0 : mTitle.hashCode());
406        return h;
407    }
408
409    /**
410     * Gets a URL pointing to an icon to use for this action, if any.
411     *
412     * @return a URL pointing to an icon to use for this action, or null
413     */
414    public URL getIconUrl() {
415        return mIconUrl;
416    }
417
418    /**
419     * Sets a URL pointing to an icon to use for this action, if any.
420     *
421     * @param iconUrl a URL pointing to an icon to use for this action, or null
422     * @return this action, to allow setter chaining
423     */
424    @NonNull
425    public RuleAction setIconUrl(URL iconUrl) {
426        mIconUrl = iconUrl;
427
428        return this;
429    }
430
431    /**
432     * Return a priority used for sorting this action
433     *
434     * @return a priority used for sorting this action
435     */
436    public int getSortPriority() {
437        return mSortPriority;
438    }
439
440    /**
441     * Returns the callback executed when the action is selected in the
442     * context menu. Cannot be null.
443     *
444     * @return the callback, never null
445     */
446    @NonNull
447    public IMenuCallback getCallback() {
448        return mCallback;
449    }
450
451    // Implements Comparable<MenuAction>
452    @Override
453    public int compareTo(RuleAction other) {
454        if (mSortPriority != other.mSortPriority) {
455            return mSortPriority - other.mSortPriority;
456        }
457
458        return mTitle.compareTo(other.mTitle);
459    }
460
461    @NonNull
462    @Override
463    public String toString() {
464        return "RuleAction [id=" + mId + ", title=" + mTitle + ", priority=" + mSortPriority + "]";
465    }
466
467    /** A separator to display between actions */
468    public static class Separator extends RuleAction {
469        /** Construct using the factory {@link #createSeparator(int)} */
470        private Separator(int sortPriority, boolean supportsMultipleNodes) {
471            super("_separator", "", IMenuCallback.NONE, sortPriority,  //$NON-NLS-1$ //$NON-NLS-2$
472                    supportsMultipleNodes);
473        }
474    }
475
476    /**
477     * A toggle is a simple on/off action, displayed as an item in a context menu
478     * with a check mark if the item is checked.
479     * <p/>
480     * Two toggles are equal if they have the same id, title and group-id.
481     * It is expected for the checked state and action callback to be different.
482     */
483    public static class Toggle extends RuleAction {
484        /**
485         * True if the item is displayed with a check mark.
486         */
487        private final boolean mIsChecked;
488
489        /**
490         * Creates a new immutable toggle action.
491         *
492         * @param id The unique id of the action. Cannot be null.
493         * @param title The UI-visible title of the context menu item. Cannot be null.
494         * @param isChecked Whether the context menu item has a check mark.
495         * @param callback A callback to execute when the context menu item is
496         *            selected.
497         */
498        private Toggle(
499                @NonNull String id,
500                @NonNull String title,
501                boolean isChecked,
502                @NonNull IMenuCallback callback,
503                int sortPriority,
504                boolean supportsMultipleNodes) {
505            super(id, title, callback, sortPriority, supportsMultipleNodes);
506            mIsChecked = isChecked;
507        }
508
509        /**
510         * Returns true if the item is displayed with a check mark.
511         *
512         * @return true if the item is displayed with a check mark.
513         */
514        public boolean isChecked() {
515            return mIsChecked;
516        }
517
518        /**
519         * Two toggles are equal if they have the same id and title.
520         * It is acceptable for the checked state and action callback to be different.
521         */
522        @Override
523        public boolean equals(Object obj) {
524            return super.equals(obj);
525        }
526
527        /**
528         * Two toggles have the same hash code if they have the same id and title.
529         */
530        @Override
531        public int hashCode() {
532            return super.hashCode();
533        }
534    }
535
536    /**
537     * An ordered list of choices the user can choose between. For choosing between
538     * actions, there is a {@link NestedAction} class.
539     */
540    public static class Choices extends RuleAction {
541        protected List<String> mTitles;
542        protected List<URL> mIconUrls;
543        protected List<String> mIds;
544        private boolean mRadio;
545
546        /**
547         * One or more id for the checked choice(s) that will be check marked.
548         * Can be null. Can be an id not present in the choices map.
549         */
550        protected final String mCurrent;
551
552        private Choices(
553                @NonNull String id,
554                @NonNull String title,
555                @NonNull IMenuCallback callback,
556                @NonNull List<String> titles,
557                @Nullable List<URL> iconUrls,
558                @NonNull List<String> ids,
559                @Nullable String current,
560                int sortPriority,
561                boolean supportsMultipleNodes) {
562            super(id, title, callback, sortPriority, supportsMultipleNodes);
563            mTitles = titles;
564            mIconUrls = iconUrls;
565            mIds = ids;
566            mCurrent = current;
567        }
568
569        /**
570         * Returns the list of urls to icons to display for each choice, or null
571         *
572         * @return the list of urls to icons to display for each choice, or null
573         */
574        @Nullable
575        public List<URL> getIconUrls() {
576            return mIconUrls;
577        }
578
579        /**
580         * Returns the list of ids for the menu choices, never null
581         *
582         * @return the list of ids for the menu choices, never null
583         */
584        @NonNull
585        public List<String> getIds() {
586            return mIds;
587        }
588
589        /**
590         * Returns the titles to be displayed for the menu choices, never null
591         *
592         * @return the titles to be displayed for the menu choices, never null
593         */
594        @NonNull
595        public List<String> getTitles() {
596            return mTitles;
597        }
598
599        /**
600         * Returns the current value of the choice
601         *
602         * @return the current value of the choice, possibly null
603         */
604        @Nullable
605        public String getCurrent() {
606            return mCurrent;
607        }
608
609        /**
610         * Set whether this choice list is best visualized as a radio group (instead of a
611         * dropdown)
612         *
613         * @param radio true if this choice list should be visualized as a radio group
614         */
615        public void setRadio(boolean radio) {
616            mRadio = radio;
617        }
618
619        /**
620         * Returns true if this choice list is best visualized as a radio group (instead
621         * of a dropdown)
622         *
623         * @return true if this choice list should be visualized as a radio group
624         */
625        public boolean isRadio() {
626            return mRadio;
627        }
628    }
629
630    /**
631     * An ordered list of actions the user can choose between. Similar to
632     * {@link Choices} but for actions instead.
633     */
634    public static class NestedAction extends RuleAction {
635        /** The provider to produce the list of nested actions when needed */
636        private final ActionProvider mProvider;
637
638        private NestedAction(
639                @NonNull String id,
640                @NonNull String title,
641                @NonNull ActionProvider provider,
642                @NonNull IMenuCallback callback,
643                int sortPriority,
644                boolean supportsMultipleNodes) {
645            super(id, title, callback, sortPriority, supportsMultipleNodes);
646            mProvider = provider;
647        }
648
649        /**
650         * Returns the nested actions available for the given node
651         *
652         * @param node the node to look up nested actions for
653         * @return a list of nested actions
654         */
655        @NonNull
656        public List<RuleAction> getNestedActions(@NonNull INode node) {
657            return mProvider.getNestedActions(node);
658        }
659    }
660
661    /** Like {@link Choices}, but the set of choices is computed lazily */
662    private static class DelayedChoices extends Choices {
663        private final ChoiceProvider mProvider;
664        private boolean mInitialized;
665
666        private DelayedChoices(
667                @NonNull String id,
668                @NonNull String title,
669                @NonNull IMenuCallback callback,
670                @Nullable String current,
671                @NonNull ChoiceProvider provider,
672                int sortPriority, boolean supportsMultipleNodes) {
673            super(id, title, callback, new ArrayList<String>(), new ArrayList<URL>(),
674                    new ArrayList<String>(), current, sortPriority, supportsMultipleNodes);
675            mProvider = provider;
676        }
677
678        private void ensureInitialized() {
679            if (!mInitialized) {
680                mInitialized = true;
681                mProvider.addChoices(mTitles, mIconUrls, mIds);
682            }
683        }
684
685        @Override
686        public List<URL> getIconUrls() {
687            ensureInitialized();
688            return mIconUrls;
689        }
690
691        @Override
692        public @NonNull List<String> getIds() {
693            ensureInitialized();
694            return mIds;
695        }
696
697        @Override
698        public @NonNull List<String> getTitles() {
699            ensureInitialized();
700            return mTitles;
701        }
702    }
703
704    /**
705     * Provides the set of nested action choices associated with a {@link NestedAction}
706     * object when they are needed. Useful for lazy initialization of context
707     * menus and popup menus until they are actually needed.
708     */
709    public interface ActionProvider {
710        /**
711         * Returns the nested actions available for the given node
712         *
713         * @param node the node to look up nested actions for
714         * @return a list of nested actions
715         */
716        @NonNull
717        public List<RuleAction> getNestedActions(@NonNull INode node);
718    }
719
720    /**
721     * Provides the set of choices associated with an {@link Choices}
722     * object when they are needed. Useful for lazy initialization of context
723     * menus and popup menus until they are actually needed.
724     */
725    public interface ChoiceProvider {
726        /**
727         * Adds in the needed titles, iconUrls (if any) and ids.
728         * Use {@link RuleAction#SEPARATOR} to create separators.
729         *
730         * @param titles a list of titles that the provider should append to
731         * @param iconUrls a list of icon URLs that the provider should append to
732         * @param ids a list of ids that the provider should append to
733         */
734        public void addChoices(
735                @NonNull List<String> titles,
736                @NonNull List<URL> iconUrls,
737                @NonNull List<String> ids);
738    }
739}
740