diff --git a/app/src/main/java/ch/blinkenlights/android/vanilla/FileSystemAdapter.java b/app/src/main/java/ch/blinkenlights/android/vanilla/FileSystemAdapter.java index cb4b1311..626c0bb1 100644 --- a/app/src/main/java/ch/blinkenlights/android/vanilla/FileSystemAdapter.java +++ b/app/src/main/java/ch/blinkenlights/android/vanilla/FileSystemAdapter.java @@ -457,13 +457,16 @@ public class FileSystemAdapter * Context menu of a row: this was dispatched by LibraryPagerAdapter * * @param intent likely created by createData() + * @param view the parent view + * @param x x-coords of event + * @param y y-coords of event */ - public boolean onCreateFancyMenu(Intent intent) { + public boolean onCreateFancyMenu(Intent intent, View view, float x, float y) { String path = intent.getStringExtra(LibraryAdapter.DATA_FILE); boolean isParentRow = (path != null && pointsToParentFolder(new File(path))); if (!isParentRow) - return mActivity.onCreateFancyMenu(intent); + return mActivity.onCreateFancyMenu(intent, view, x, y); // else: no context menu, but consume event. return true; } diff --git a/app/src/main/java/ch/blinkenlights/android/vanilla/LibraryActivity.java b/app/src/main/java/ch/blinkenlights/android/vanilla/LibraryActivity.java index 476fbae5..6514dbe4 100644 --- a/app/src/main/java/ch/blinkenlights/android/vanilla/LibraryActivity.java +++ b/app/src/main/java/ch/blinkenlights/android/vanilla/LibraryActivity.java @@ -627,13 +627,13 @@ public class LibraryActivity /** * Creates a context menu for an adapter row. * - * @param menu The menu to create. * @param rowData Data for the adapter row. + * @param view the view which was clicked. + * @param x x-coords of event + * @param y y-coords of event */ - public boolean onCreateFancyMenu(Intent rowData) { + public boolean onCreateFancyMenu(Intent rowData, View view, float x, float y) { FancyMenu fm = new FancyMenu(this, this); - fm.show(getFragmentManager(), "LibraryActivityContext"); - // Add to playlist is always available. fm.addSpacer(20); fm.add(CTX_MENU_ADD_TO_PLAYLIST, 20, R.drawable.menu_add_to_playlist, R.string.add_to_playlist).setIntent(rowData); @@ -682,6 +682,7 @@ public class LibraryActivity fm.addSpacer(90); fm.add(CTX_MENU_DELETE, 90, R.drawable.menu_delete, R.string.delete).setIntent(rowData); } + fm.show(view, x, y); return true; } diff --git a/app/src/main/java/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java b/app/src/main/java/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java index 2fda62df..ed2addc8 100644 --- a/app/src/main/java/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java +++ b/app/src/main/java/ch/blinkenlights/android/vanilla/LibraryPagerAdapter.java @@ -23,6 +23,8 @@ package ch.blinkenlights.android.vanilla; +import ch.blinkenlights.android.vanilla.ext.CoordClickListener; + import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; @@ -50,7 +52,7 @@ public class LibraryPagerAdapter extends PagerAdapter implements Handler.Callback , ViewPager.OnPageChangeListener - , AdapterView.OnItemLongClickListener + , CoordClickListener.Callback , AdapterView.OnItemClickListener { /** @@ -323,11 +325,12 @@ public class LibraryPagerAdapter throw new IllegalArgumentException("Invalid media type: " + type); } + CoordClickListener ccl = new CoordClickListener(this); view = (ListView)inflater.inflate(R.layout.listview, null); - view.setOnItemLongClickListener(this); + ccl.registerForOnItemLongClickListener(view); view.setOnItemClickListener(this); - view.setTag(type); + if (header != null) { header.setText(mHeaderText); header.setTag(new ViewHolder()); // behave like a normal library row @@ -859,17 +862,19 @@ public class LibraryPagerAdapter * @param view the long clicked view * @param position row position * @param id id of the long clicked row + * @param x x-coords of event + * @param y y-coords of event * * @return true if the event was consumed */ - public boolean onItemLongClick (AdapterView parent, View view, int position, long id) { + public boolean onItemLongClickWithCoords (AdapterView parent, View view, int position, long id, float x, float y) { Intent intent = id == LibraryAdapter.HEADER_ID ? createHeaderIntent(view) : mCurrentAdapter.createData(view); int type = (Integer)((View)view.getParent()).getTag(); if (type == MediaUtils.TYPE_FILE) { - return mFilesAdapter.onCreateFancyMenu(intent); + return mFilesAdapter.onCreateFancyMenu(intent, view, x, y); } - return mActivity.onCreateFancyMenu(intent); + return mActivity.onCreateFancyMenu(intent, view, x, y); } @Override diff --git a/app/src/main/java/ch/blinkenlights/android/vanilla/PlaylistActivity.java b/app/src/main/java/ch/blinkenlights/android/vanilla/PlaylistActivity.java index 1a1ae396..6a490361 100644 --- a/app/src/main/java/ch/blinkenlights/android/vanilla/PlaylistActivity.java +++ b/app/src/main/java/ch/blinkenlights/android/vanilla/PlaylistActivity.java @@ -207,7 +207,6 @@ public class PlaylistActivity extends Activity intent.putExtra("audioId", holder.id); FancyMenu fm = new FancyMenu(this, this); - fm.show(getFragmentManager(), "PlaylistActivityContext"); fm.setHeaderTitle(holder.title); fm.add(MENU_PLAY, 0, R.drawable.menu_play, R.string.play).setIntent(intent); @@ -221,6 +220,7 @@ public class PlaylistActivity extends Activity fm.addSpacer(0); fm.add(MENU_SHOW_DETAILS, 0, R.drawable.menu_details, R.string.details).setIntent(intent); fm.add(MENU_REMOVE, 0, R.drawable.menu_remove, R.string.remove).setIntent(intent); + fm.show(view); return true; } diff --git a/app/src/main/java/ch/blinkenlights/android/vanilla/ShowQueueFragment.java b/app/src/main/java/ch/blinkenlights/android/vanilla/ShowQueueFragment.java index a561890d..6cfe9c80 100644 --- a/app/src/main/java/ch/blinkenlights/android/vanilla/ShowQueueFragment.java +++ b/app/src/main/java/ch/blinkenlights/android/vanilla/ShowQueueFragment.java @@ -108,7 +108,6 @@ public class ShowQueueFragment extends Fragment intent.putExtra("position", pos); FancyMenu fm = new FancyMenu(getActivity(), this); - fm.show(getFragmentManager(), "ShowQueueFragmentContext"); fm.setHeaderTitle(song.title); fm.add(CTX_MENU_PLAY, 0, R.drawable.menu_play, R.string.play).setIntent(intent); @@ -122,6 +121,7 @@ public class ShowQueueFragment extends Fragment fm.addSpacer(0); fm.add(CTX_MENU_SHOW_DETAILS, 0, R.drawable.menu_details, R.string.details).setIntent(intent); fm.add(CTX_MENU_REMOVE, 90, R.drawable.menu_remove, R.string.remove).setIntent(intent); + fm.show(view); return true; } diff --git a/app/src/main/java/ch/blinkenlights/android/vanilla/ext/CoordClickListener.java b/app/src/main/java/ch/blinkenlights/android/vanilla/ext/CoordClickListener.java new file mode 100644 index 00000000..664bc588 --- /dev/null +++ b/app/src/main/java/ch/blinkenlights/android/vanilla/ext/CoordClickListener.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2018 Adrian Ulrich + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package ch.blinkenlights.android.vanilla.ext; + +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.widget.AdapterView; +import android.widget.ListView; + + +public class CoordClickListener + implements + AdapterView.OnItemLongClickListener + , View.OnTouchListener { + /** + * Interface to implement by the callback + */ + public interface Callback { + boolean onItemLongClickWithCoords(AdapterView parent, View view, int position, long id, float x, float y); + } + /** + * The registered callback consumer + */ + private Callback mCallback; + /** + * Last X position seen. + */ + private float mPosX; + /** + * Last Y position seen. + */ + private float mPosY; + + /** + * Create a new CoordClickListener instance + * + * @param cb the callback consumer. + */ + public CoordClickListener(Callback cb) { + mCallback = cb; + } + + /** + * Register a view for long click observation. + * + * @param view the view to listen for long clicks + */ + public void registerForOnItemLongClickListener(ListView view) { + view.setOnItemLongClickListener(this); + view.setOnTouchListener(this); + } + + /** + * Implementation of onItemLongClickListener interface + */ + @Override + public boolean onItemLongClick (AdapterViewparent, View view, int position, long id) { + return mCallback.onItemLongClickWithCoords(parent, view, position, id, mPosX, mPosY); + } + + /** + * Implementation of OnTouchListener interface + */ + @Override + public boolean onTouch(View view, MotionEvent ev) { + mPosX = ev.getX(); + mPosY = ev.getY(); + // Not handled: we just observe. + return false; + } +} diff --git a/app/src/main/java/ch/blinkenlights/android/vanilla/ui/FancyMenu.java b/app/src/main/java/ch/blinkenlights/android/vanilla/ui/FancyMenu.java index bd293527..62b5cec2 100644 --- a/app/src/main/java/ch/blinkenlights/android/vanilla/ui/FancyMenu.java +++ b/app/src/main/java/ch/blinkenlights/android/vanilla/ui/FancyMenu.java @@ -25,26 +25,23 @@ package ch.blinkenlights.android.vanilla.ui; import ch.blinkenlights.android.vanilla.R; import ch.blinkenlights.android.vanilla.ThemeHelper; -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.DialogFragment; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; -import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.AdapterView; import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.ListPopupWindow; import android.widget.TextView; import android.widget.ImageView; +import android.util.DisplayMetrics; import java.util.ArrayList; -import android.util.Log; -public class FancyMenu extends DialogFragment - implements DialogInterface.OnClickListener { +public class FancyMenu { /** * Title to use for this FancyMenu */ @@ -61,10 +58,6 @@ public class FancyMenu extends DialogFragment * List of all items and possible children */ private ArrayList> mItems; - /** - * The built adapter used by the visible dialog - */ - private FancyMenu.Adapter mAdapter; /** * The callback interface to implement */ @@ -108,10 +101,11 @@ public class FancyMenu extends DialogFragment /** * Adds a new item to the menu * - * @param id the id which identifies this object. + * @param id the id which identifies this object * @param order how to sort this item * @param drawable icon drawable to use * @param text string label + * @return a new FancyMenuItem */ public FancyMenuItem add(int id, int order, int drawable, CharSequence text) { return addInternal(id, order, drawable, text, false); @@ -121,11 +115,22 @@ public class FancyMenu extends DialogFragment * Adds a new spacer item * * @param order how to sort this item + * @return a new FancyMenuItem */ public FancyMenuItem addSpacer(int order) { return addInternal(0, order, 0, null, true); } + /** + * Internal add implementation + * + * @param id the id which identifies the object + * @param order how to sort this item + * @param icon the icon resource to use + * @param text the text label to display + * @param spacer whether or not this is a spacer + * @return a new FancyMenuItem + */ private FancyMenuItem addInternal(int id, int order, int icon, CharSequence text, boolean spacer) { FancyMenuItem item = new FancyMenuItem(mContext, id) .setIcon(icon) @@ -140,45 +145,99 @@ public class FancyMenu extends DialogFragment } /** - * Called when this dialog is about to be shown. + * Creates an Adapter from a nested ArrayList + * + * @param items the nested list containing the items + * @return the new adapter */ - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - + private Adapter assembleAdapter(ArrayList> items) { + final Adapter adapter = new Adapter(mContext, 0); // spacers look awful on holo themes final boolean usesSpacers = !ThemeHelper.usesHoloTheme(); - - // The adaper will back this list - mAdapter = new FancyMenu.Adapter(mContext, 0); - for (ArrayList sub : mItems) { + for (ArrayList sub : items) { for (FancyMenuItem item : sub ) { if (usesSpacers || !item.isSpacer()) { - mAdapter.add(item); + adapter.add(item); } } } - - AlertDialog.Builder builder = new AlertDialog.Builder(mContext); - builder.setTitle(mTitle) - .setAdapter(mAdapter, this); - - return builder.create(); + return adapter; } /** - * Callback for click events on the displayed dialog + * Measures the predicted total height and max width of given adapter. + * + * @param adapter the adapter to measure + * @param result int array with two elements. 0 is the max height, 1 the longest width. */ - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - - FancyMenuItem item = mAdapter.getItem(which); - if (!item.isSpacer()) { - mCallback.onFancyItemSelected(item); + private void measureAdapter(Adapter adapter, int[] result) { + result[0] = 0; + result[1] = 0; + for (int i = 0; i < adapter.getCount(); i++) { + View view = adapter.getView(i, null, null); + view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); + result[0] += view.getMeasuredHeight(); + if (result[1] < view.getMeasuredWidth()) { + result[1] = view.getMeasuredWidth(); + } } } + /** + * Calculate the height that should be used for the menu + * + * @param the estimated height + * @return the height to use instead of the input + */ + private int getMenuHeight(int suggested) { + DisplayMetrics metrics = mContext.getResources().getDisplayMetrics(); + int maxHeight = (int)((float)metrics.heightPixels * 0.35); + return (suggested > maxHeight ? maxHeight : ListPopupWindow.WRAP_CONTENT); + } + + public void show(View parent) { + show(parent, Float.NaN, Float.NaN); + } + + /** + * Show the assembled fancy menu + * + * @param the parent view to use as anchor + * @param x x-coord position hint + * @param y y-coord position hint + */ + public void show(View parent, float x, float y) { + final ListPopupWindow pw = new ListPopupWindow(mContext); + final Adapter adapter = assembleAdapter(mItems); + AdapterView.OnItemClickListener listener = new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int pos, long id) { + FancyMenuItem item = adapter.getItem(pos); + if (!item.isSpacer()) { + mCallback.onFancyItemSelected(item); + } + pw.dismiss(); + } + }; + + int result[] = new int[2]; + measureAdapter(adapter, result); + pw.setHeight(getMenuHeight(result[0])); + pw.setWidth(result[1]); + + pw.setAdapter(adapter); + pw.setOnItemClickListener(listener); + pw.setModal(true); + pw.setAnchorView(parent); + + if (!Float.isNaN(x) && !Float.isNaN(y)) { + parent.getLocationInWindow(result); + pw.setHorizontalOffset((int)x - result[0]); + } + + pw.show(); + } /** * Adapter class backing all menu entries