1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
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.internal.view.menu;
18
19import android.content.Context;
20import android.content.res.Configuration;
21import android.content.res.Resources;
22import android.content.res.TypedArray;
23import android.graphics.Rect;
24import android.graphics.drawable.Drawable;
25import android.os.Parcelable;
26import android.text.TextUtils;
27import android.util.AttributeSet;
28import android.view.Gravity;
29import android.view.MotionEvent;
30import android.view.View;
31import android.view.accessibility.AccessibilityEvent;
32import android.widget.ActionMenuView;
33import android.widget.ForwardingListener;
34import android.widget.TextView;
35import android.widget.Toast;
36
37/**
38 * @hide
39 */
40public class ActionMenuItemView extends TextView
41        implements MenuView.ItemView, View.OnClickListener, View.OnLongClickListener,
42        ActionMenuView.ActionMenuChildView {
43    private static final String TAG = "ActionMenuItemView";
44
45    private MenuItemImpl mItemData;
46    private CharSequence mTitle;
47    private Drawable mIcon;
48    private MenuBuilder.ItemInvoker mItemInvoker;
49    private ForwardingListener mForwardingListener;
50    private PopupCallback mPopupCallback;
51
52    private boolean mAllowTextWithIcon;
53    private boolean mExpandedFormat;
54    private int mMinWidth;
55    private int mSavedPaddingLeft;
56
57    private static final int MAX_ICON_SIZE = 32; // dp
58    private int mMaxIconSize;
59
60    public ActionMenuItemView(Context context) {
61        this(context, null);
62    }
63
64    public ActionMenuItemView(Context context, AttributeSet attrs) {
65        this(context, attrs, 0);
66    }
67
68    public ActionMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) {
69        this(context, attrs, defStyleAttr, 0);
70    }
71
72    public ActionMenuItemView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
73        super(context, attrs, defStyleAttr, defStyleRes);
74        final Resources res = context.getResources();
75        mAllowTextWithIcon = shouldAllowTextWithIcon();
76        final TypedArray a = context.obtainStyledAttributes(attrs,
77                com.android.internal.R.styleable.ActionMenuItemView, defStyleAttr, defStyleRes);
78        mMinWidth = a.getDimensionPixelSize(
79                com.android.internal.R.styleable.ActionMenuItemView_minWidth, 0);
80        a.recycle();
81
82        final float density = res.getDisplayMetrics().density;
83        mMaxIconSize = (int) (MAX_ICON_SIZE * density + 0.5f);
84
85        setOnClickListener(this);
86        setOnLongClickListener(this);
87
88        mSavedPaddingLeft = -1;
89        setSaveEnabled(false);
90    }
91
92    @Override
93    public void onConfigurationChanged(Configuration newConfig) {
94        super.onConfigurationChanged(newConfig);
95
96        mAllowTextWithIcon = shouldAllowTextWithIcon();
97        updateTextButtonVisibility();
98    }
99
100    /**
101     * Whether action menu items should obey the "withText" showAsAction flag. This may be set to
102     * false for situations where space is extremely limited. -->
103     */
104    private boolean shouldAllowTextWithIcon() {
105        final Configuration configuration = getContext().getResources().getConfiguration();
106        final int width = configuration.screenWidthDp;
107        final int height = configuration.screenHeightDp;
108        return  width >= 480 || (width >= 640 && height >= 480)
109                || configuration.orientation == Configuration.ORIENTATION_LANDSCAPE;
110    }
111
112    @Override
113    public void setPadding(int l, int t, int r, int b) {
114        mSavedPaddingLeft = l;
115        super.setPadding(l, t, r, b);
116    }
117
118    public MenuItemImpl getItemData() {
119        return mItemData;
120    }
121
122    @Override
123    public void initialize(MenuItemImpl itemData, int menuType) {
124        mItemData = itemData;
125
126        setIcon(itemData.getIcon());
127        setTitle(itemData.getTitleForItemView(this)); // Title only takes effect if there is no icon
128        setId(itemData.getItemId());
129
130        setVisibility(itemData.isVisible() ? View.VISIBLE : View.GONE);
131        setEnabled(itemData.isEnabled());
132
133        if (itemData.hasSubMenu()) {
134            if (mForwardingListener == null) {
135                mForwardingListener = new ActionMenuItemForwardingListener();
136            }
137        }
138    }
139
140    @Override
141    public boolean onTouchEvent(MotionEvent e) {
142        if (mItemData.hasSubMenu() && mForwardingListener != null
143                && mForwardingListener.onTouch(this, e)) {
144            return true;
145        }
146        return super.onTouchEvent(e);
147    }
148
149    @Override
150    public void onClick(View v) {
151        if (mItemInvoker != null) {
152            mItemInvoker.invokeItem(mItemData);
153        }
154    }
155
156    public void setItemInvoker(MenuBuilder.ItemInvoker invoker) {
157        mItemInvoker = invoker;
158    }
159
160    public void setPopupCallback(PopupCallback popupCallback) {
161        mPopupCallback = popupCallback;
162    }
163
164    public boolean prefersCondensedTitle() {
165        return true;
166    }
167
168    public void setCheckable(boolean checkable) {
169        // TODO Support checkable action items
170    }
171
172    public void setChecked(boolean checked) {
173        // TODO Support checkable action items
174    }
175
176    public void setExpandedFormat(boolean expandedFormat) {
177        if (mExpandedFormat != expandedFormat) {
178            mExpandedFormat = expandedFormat;
179            if (mItemData != null) {
180                mItemData.actionFormatChanged();
181            }
182        }
183    }
184
185    private void updateTextButtonVisibility() {
186        boolean visible = !TextUtils.isEmpty(mTitle);
187        visible &= mIcon == null ||
188                (mItemData.showsTextAsAction() && (mAllowTextWithIcon || mExpandedFormat));
189
190        setText(visible ? mTitle : null);
191    }
192
193    public void setIcon(Drawable icon) {
194        mIcon = icon;
195        if (icon != null) {
196            int width = icon.getIntrinsicWidth();
197            int height = icon.getIntrinsicHeight();
198            if (width > mMaxIconSize) {
199                final float scale = (float) mMaxIconSize / width;
200                width = mMaxIconSize;
201                height *= scale;
202            }
203            if (height > mMaxIconSize) {
204                final float scale = (float) mMaxIconSize / height;
205                height = mMaxIconSize;
206                width *= scale;
207            }
208            icon.setBounds(0, 0, width, height);
209        }
210        setCompoundDrawables(icon, null, null, null);
211
212        updateTextButtonVisibility();
213    }
214
215    public boolean hasText() {
216        return !TextUtils.isEmpty(getText());
217    }
218
219    public void setShortcut(boolean showShortcut, char shortcutKey) {
220        // Action buttons don't show text for shortcut keys.
221    }
222
223    public void setTitle(CharSequence title) {
224        mTitle = title;
225
226        setContentDescription(mTitle);
227        updateTextButtonVisibility();
228    }
229
230    @Override
231    public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
232        onPopulateAccessibilityEvent(event);
233        return true;
234    }
235
236    @Override
237    public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
238        super.onPopulateAccessibilityEventInternal(event);
239        final CharSequence cdesc = getContentDescription();
240        if (!TextUtils.isEmpty(cdesc)) {
241            event.getText().add(cdesc);
242        }
243    }
244
245    @Override
246    public boolean dispatchHoverEvent(MotionEvent event) {
247        // Don't allow children to hover; we want this to be treated as a single component.
248        return onHoverEvent(event);
249    }
250
251    public boolean showsIcon() {
252        return true;
253    }
254
255    public boolean needsDividerBefore() {
256        return hasText() && mItemData.getIcon() == null;
257    }
258
259    public boolean needsDividerAfter() {
260        return hasText();
261    }
262
263    @Override
264    public boolean onLongClick(View v) {
265        if (hasText()) {
266            // Don't show the cheat sheet for items that already show text.
267            return false;
268        }
269
270        final int[] screenPos = new int[2];
271        final Rect displayFrame = new Rect();
272        getLocationOnScreen(screenPos);
273        getWindowVisibleDisplayFrame(displayFrame);
274
275        final Context context = getContext();
276        final int width = getWidth();
277        final int height = getHeight();
278        final int midy = screenPos[1] + height / 2;
279        int referenceX = screenPos[0] + width / 2;
280        if (v.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR) {
281            final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
282            referenceX = screenWidth - referenceX; // mirror
283        }
284        Toast cheatSheet = Toast.makeText(context, mItemData.getTitle(), Toast.LENGTH_SHORT);
285        if (midy < displayFrame.height()) {
286            // Show along the top; follow action buttons
287            cheatSheet.setGravity(Gravity.TOP | Gravity.END, referenceX,
288                    screenPos[1] + height - displayFrame.top);
289        } else {
290            // Show along the bottom center
291            cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height);
292        }
293        cheatSheet.show();
294        return true;
295    }
296
297    @Override
298    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
299        final boolean textVisible = hasText();
300        if (textVisible && mSavedPaddingLeft >= 0) {
301            super.setPadding(mSavedPaddingLeft, getPaddingTop(),
302                    getPaddingRight(), getPaddingBottom());
303        }
304
305        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
306
307        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
308        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
309        final int oldMeasuredWidth = getMeasuredWidth();
310        final int targetWidth = widthMode == MeasureSpec.AT_MOST ? Math.min(widthSize, mMinWidth)
311                : mMinWidth;
312
313        if (widthMode != MeasureSpec.EXACTLY && mMinWidth > 0 && oldMeasuredWidth < targetWidth) {
314            // Remeasure at exactly the minimum width.
315            super.onMeasure(MeasureSpec.makeMeasureSpec(targetWidth, MeasureSpec.EXACTLY),
316                    heightMeasureSpec);
317        }
318
319        if (!textVisible && mIcon != null) {
320            // TextView won't center compound drawables in both dimensions without
321            // a little coercion. Pad in to center the icon after we've measured.
322            final int w = getMeasuredWidth();
323            final int dw = mIcon.getBounds().width();
324            super.setPadding((w - dw) / 2, getPaddingTop(), getPaddingRight(), getPaddingBottom());
325        }
326    }
327
328    private class ActionMenuItemForwardingListener extends ForwardingListener {
329        public ActionMenuItemForwardingListener() {
330            super(ActionMenuItemView.this);
331        }
332
333        @Override
334        public ShowableListMenu getPopup() {
335            if (mPopupCallback != null) {
336                return mPopupCallback.getPopup();
337            }
338            return null;
339        }
340
341        @Override
342        protected boolean onForwardingStarted() {
343            // Call the invoker, then check if the expected popup is showing.
344            if (mItemInvoker != null && mItemInvoker.invokeItem(mItemData)) {
345                final ShowableListMenu popup = getPopup();
346                return popup != null && popup.isShowing();
347            }
348            return false;
349        }
350    }
351
352    @Override
353    public void onRestoreInstanceState(Parcelable state) {
354        // This might get called with the state of ActionView since it shares the same ID with
355        // ActionMenuItemView. Do not restore this state as ActionMenuItemView never saved it.
356        super.onRestoreInstanceState(null);
357    }
358
359    public static abstract class PopupCallback {
360        public abstract ShowableListMenu getPopup();
361    }
362}
363