michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.home; michael@0: michael@0: import java.util.EnumSet; michael@0: michael@0: import org.mozilla.gecko.R; michael@0: import org.mozilla.gecko.Telemetry; michael@0: import org.mozilla.gecko.TelemetryContract; michael@0: import org.mozilla.gecko.ThumbnailHelper; michael@0: import org.mozilla.gecko.db.BrowserDB.URLColumns; michael@0: import org.mozilla.gecko.db.TopSitesCursorWrapper; michael@0: import org.mozilla.gecko.home.HomePager.OnUrlOpenListener; michael@0: michael@0: import android.content.Context; michael@0: import android.content.res.TypedArray; michael@0: import android.database.Cursor; michael@0: import android.graphics.Rect; michael@0: import android.text.TextUtils; michael@0: import android.util.AttributeSet; michael@0: import android.view.ContextMenu.ContextMenuInfo; michael@0: import android.view.View; michael@0: import android.widget.AbsListView; michael@0: import android.widget.AdapterView; michael@0: import android.widget.GridView; michael@0: michael@0: /** michael@0: * A grid view of top and pinned sites. michael@0: * Each cell in the grid is a TopSitesGridItemView. michael@0: */ michael@0: public class TopSitesGridView extends GridView { michael@0: private static final String LOGTAG = "GeckoTopSitesGridView"; michael@0: michael@0: // Listener for editing pinned sites. michael@0: public static interface OnEditPinnedSiteListener { michael@0: public void onEditPinnedSite(int position, String searchTerm); michael@0: } michael@0: michael@0: // Max number of top sites that needs to be shown. michael@0: private final int mMaxSites; michael@0: michael@0: // Number of columns to show. michael@0: private final int mNumColumns; michael@0: michael@0: // Horizontal spacing in between the rows. michael@0: private final int mHorizontalSpacing; michael@0: michael@0: // Vertical spacing in between the rows. michael@0: private final int mVerticalSpacing; michael@0: michael@0: // Measured width of this view. michael@0: private int mMeasuredWidth; michael@0: michael@0: // Measured height of this view. michael@0: private int mMeasuredHeight; michael@0: michael@0: // On URL open listener. michael@0: private OnUrlOpenListener mUrlOpenListener; michael@0: michael@0: // Edit pinned site listener. michael@0: private OnEditPinnedSiteListener mEditPinnedSiteListener; michael@0: michael@0: // Context menu info. michael@0: private TopSitesGridContextMenuInfo mContextMenuInfo; michael@0: michael@0: // Whether we're handling focus changes or not. This is used michael@0: // to avoid infinite re-layouts when using this GridView as michael@0: // a ListView header view (see bug 918044). michael@0: private boolean mIsHandlingFocusChange; michael@0: michael@0: public TopSitesGridView(Context context) { michael@0: this(context, null); michael@0: } michael@0: michael@0: public TopSitesGridView(Context context, AttributeSet attrs) { michael@0: this(context, attrs, R.attr.topSitesGridViewStyle); michael@0: } michael@0: michael@0: public TopSitesGridView(Context context, AttributeSet attrs, int defStyle) { michael@0: super(context, attrs, defStyle); michael@0: mMaxSites = getResources().getInteger(R.integer.number_of_top_sites); michael@0: mNumColumns = getResources().getInteger(R.integer.number_of_top_sites_cols); michael@0: setNumColumns(mNumColumns); michael@0: michael@0: TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TopSitesGridView, defStyle, 0); michael@0: mHorizontalSpacing = a.getDimensionPixelOffset(R.styleable.TopSitesGridView_android_horizontalSpacing, 0x00); michael@0: mVerticalSpacing = a.getDimensionPixelOffset(R.styleable.TopSitesGridView_android_verticalSpacing, 0x00); michael@0: a.recycle(); michael@0: michael@0: mIsHandlingFocusChange = false; michael@0: } michael@0: michael@0: /** michael@0: * {@inheritDoc} michael@0: */ michael@0: @Override michael@0: public void onAttachedToWindow() { michael@0: super.onAttachedToWindow(); michael@0: michael@0: setOnItemClickListener(new AdapterView.OnItemClickListener() { michael@0: @Override michael@0: public void onItemClick(AdapterView parent, View view, int position, long id) { michael@0: TopSitesGridItemView row = (TopSitesGridItemView) view; michael@0: michael@0: // Decode "user-entered" URLs before loading them. michael@0: String url = HomeFragment.decodeUserEnteredUrl(row.getUrl()); michael@0: michael@0: // If the url is empty, the user can pin a site. michael@0: // If not, navigate to the page given by the url. michael@0: if (!TextUtils.isEmpty(url)) { michael@0: if (mUrlOpenListener != null) { michael@0: Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.GRID_ITEM, Integer.toString(position)); michael@0: michael@0: mUrlOpenListener.onUrlOpen(url, EnumSet.noneOf(OnUrlOpenListener.Flags.class)); michael@0: } michael@0: } else { michael@0: if (mEditPinnedSiteListener != null) { michael@0: mEditPinnedSiteListener.onEditPinnedSite(position, ""); michael@0: } michael@0: } michael@0: } michael@0: }); michael@0: michael@0: setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { michael@0: @Override michael@0: public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { michael@0: Cursor cursor = (Cursor) parent.getItemAtPosition(position); michael@0: michael@0: TopSitesGridItemView gridView = (TopSitesGridItemView) view; michael@0: if (cursor == null || gridView.isEmpty()) { michael@0: mContextMenuInfo = null; michael@0: return false; michael@0: } michael@0: michael@0: mContextMenuInfo = new TopSitesGridContextMenuInfo(view, position, id); michael@0: updateContextMenuFromCursor(mContextMenuInfo, cursor); michael@0: return showContextMenuForChild(TopSitesGridView.this); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: @Override michael@0: public void onDetachedFromWindow() { michael@0: super.onDetachedFromWindow(); michael@0: michael@0: mUrlOpenListener = null; michael@0: mEditPinnedSiteListener = null; michael@0: } michael@0: michael@0: @Override michael@0: protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { michael@0: mIsHandlingFocusChange = true; michael@0: super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); michael@0: mIsHandlingFocusChange = false; michael@0: } michael@0: michael@0: @Override michael@0: public void requestLayout() { michael@0: if (!mIsHandlingFocusChange) { michael@0: super.requestLayout(); michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * {@inheritDoc} michael@0: */ michael@0: @Override michael@0: public int getColumnWidth() { michael@0: // This method will be called from onMeasure() too. michael@0: // It's better to use getMeasuredWidth(), as it is safe in this case. michael@0: final int totalHorizontalSpacing = mNumColumns > 0 ? (mNumColumns - 1) * mHorizontalSpacing : 0; michael@0: return (getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - totalHorizontalSpacing) / mNumColumns; michael@0: } michael@0: michael@0: /** michael@0: * {@inheritDoc} michael@0: */ michael@0: @Override michael@0: protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { michael@0: // Sets the padding for this view. michael@0: super.onMeasure(widthMeasureSpec, heightMeasureSpec); michael@0: michael@0: final int measuredWidth = getMeasuredWidth(); michael@0: if (measuredWidth == mMeasuredWidth) { michael@0: // Return the cached values as the width is the same. michael@0: setMeasuredDimension(mMeasuredWidth, mMeasuredHeight); michael@0: return; michael@0: } michael@0: michael@0: final int columnWidth = getColumnWidth(); michael@0: michael@0: // Get the first child from the adapter. michael@0: final TopSitesGridItemView child = new TopSitesGridItemView(getContext()); michael@0: michael@0: // Set a default LayoutParams on the child, if it doesn't have one on its own. michael@0: AbsListView.LayoutParams params = (AbsListView.LayoutParams) child.getLayoutParams(); michael@0: if (params == null) { michael@0: params = new AbsListView.LayoutParams(AbsListView.LayoutParams.WRAP_CONTENT, michael@0: AbsListView.LayoutParams.WRAP_CONTENT); michael@0: child.setLayoutParams(params); michael@0: } michael@0: michael@0: // Measure the exact width of the child, and the height based on the width. michael@0: // Note: the child (and TopSitesThumbnailView) takes care of calculating its height. michael@0: int childWidthSpec = MeasureSpec.makeMeasureSpec(columnWidth, MeasureSpec.EXACTLY); michael@0: int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); michael@0: child.measure(childWidthSpec, childHeightSpec); michael@0: final int childHeight = child.getMeasuredHeight(); michael@0: michael@0: // This is the maximum width of the contents of each child in the grid. michael@0: // Use this as the target width for thumbnails. michael@0: final int thumbnailWidth = child.getMeasuredWidth() - child.getPaddingLeft() - child.getPaddingRight(); michael@0: ThumbnailHelper.getInstance().setThumbnailWidth(thumbnailWidth); michael@0: michael@0: // Number of rows required to show these top sites. michael@0: final int rows = (int) Math.ceil((double) mMaxSites / mNumColumns); michael@0: final int childrenHeight = childHeight * rows; michael@0: final int totalVerticalSpacing = rows > 0 ? (rows - 1) * mVerticalSpacing : 0; michael@0: michael@0: // Total height of this view. michael@0: final int measuredHeight = childrenHeight + getPaddingTop() + getPaddingBottom() + totalVerticalSpacing; michael@0: setMeasuredDimension(measuredWidth, measuredHeight); michael@0: mMeasuredWidth = measuredWidth; michael@0: mMeasuredHeight = measuredHeight; michael@0: } michael@0: michael@0: @Override michael@0: public ContextMenuInfo getContextMenuInfo() { michael@0: return mContextMenuInfo; michael@0: } michael@0: michael@0: /* michael@0: * Update the fields of a TopSitesGridContextMenuInfo object michael@0: * from a cursor. michael@0: * michael@0: * @param info context menu info object to be updated michael@0: * @param cursor used to update the context menu info object michael@0: */ michael@0: private void updateContextMenuFromCursor(TopSitesGridContextMenuInfo info, Cursor cursor) { michael@0: info.url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL)); michael@0: info.title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE)); michael@0: info.isPinned = ((TopSitesCursorWrapper) cursor).isPinned(); michael@0: } michael@0: /** michael@0: * Set an url open listener to be used by this view. michael@0: * michael@0: * @param listener An url open listener for this view. michael@0: */ michael@0: public void setOnUrlOpenListener(OnUrlOpenListener listener) { michael@0: mUrlOpenListener = listener; michael@0: } michael@0: michael@0: /** michael@0: * Set an edit pinned site listener to be used by this view. michael@0: * michael@0: * @param listener An edit pinned site listener for this view. michael@0: */ michael@0: public void setOnEditPinnedSiteListener(final OnEditPinnedSiteListener listener) { michael@0: mEditPinnedSiteListener = listener; michael@0: } michael@0: michael@0: /** michael@0: * Stores information regarding the creation of the context menu for a GridView item. michael@0: */ michael@0: public static class TopSitesGridContextMenuInfo extends HomeContextMenuInfo { michael@0: public boolean isPinned = false; michael@0: michael@0: public TopSitesGridContextMenuInfo(View targetView, int position, long id) { michael@0: super(targetView, position, id); michael@0: } michael@0: } michael@0: }