From a0d576ffc3b44d100d625f05b93a091a114ea4ff Mon Sep 17 00:00:00 2001 From: evermind Date: Tue, 11 Oct 2022 15:39:47 +0200 Subject: [PATCH] searchfilters: 3rd Ui: action based UI (enhanched legacy menu) This approach is more or less a hack but if all else fails. Could later be dropped or right away. --- .../list/search/SearchFragmentLegacy.java | 73 +++++ .../filter/SearchFilterUIOptionMenu.java | 303 ++++++++++++++++++ 2 files changed, 376 insertions(+) create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragmentLegacy.java create mode 100644 app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterUIOptionMenu.java diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragmentLegacy.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragmentLegacy.java new file mode 100644 index 000000000..7186983e2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragmentLegacy.java @@ -0,0 +1,73 @@ +// Created by evermind-zz 2022, licensed GNU GPL version 3 or later + +package org.schabi.newpipe.fragments.list.search; + +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic; +import org.schabi.newpipe.fragments.list.search.filter.SearchFilterUIOptionMenu; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.Toolbar; +import androidx.core.content.ContextCompat; +import icepick.State; + +/** + * Fragment that hosts the action menu based filter 'dialog'. + *

+ * Called ..Legacy because this was the way NewPipe had implemented the search filter dialog. + *

+ * The new UI's are handled by {@link SearchFragment} and implemented by + * using {@link androidx.fragment.app.DialogFragment}. + */ +public class SearchFragmentLegacy extends SearchFragment { + + @State + protected int countOnPrepareOptionsMenuCalls = 0; + private SearchFilterUIOptionMenu searchFilterUi; + + @Override + protected void initViewModel() { + logicVariant = SearchFilterLogic.Factory.Variant.SEARCH_FILTER_LOGIC_LEGACY; + super.initViewModel(); + + searchFilterUi = new SearchFilterUIOptionMenu( + searchViewModel.getSearchFilterLogic(), requireContext()); + } + + @Override + protected void createMenu(@NonNull final Menu menu, + @NonNull final MenuInflater inflater) { + searchFilterUi.createSearchUI(menu); + } + + @Override + public boolean onOptionsItemSelected(@NonNull final MenuItem item) { + return searchFilterUi.onOptionsItemSelected(item); + } + + @Override + protected void initViews(final View rootView, + final Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + final Toolbar toolbar = (Toolbar) searchToolbarContainer.getParent(); + toolbar.setOverflowIcon(ContextCompat.getDrawable(requireContext(), + R.drawable.ic_sort)); + } + + @Override + public void onPrepareOptionsMenu(@NonNull final Menu menu) { + super.onPrepareOptionsMenu(menu); + // workaround: we want to hide the keyboard in case we open the options + // menu. As somehow this method gets triggered twice but only the 2nd + // time is relevant as the options menu is selected by the user. + if (++countOnPrepareOptionsMenuCalls > 1) { + hideKeyboardSearch(); + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterUIOptionMenu.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterUIOptionMenu.java new file mode 100644 index 000000000..605c15daf --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterUIOptionMenu.java @@ -0,0 +1,303 @@ +// Created by evermind-zz 2022, licensed GNU GPL version 3 or later + +package org.schabi.newpipe.fragments.list.search.filter; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.search.filter.FilterContainer; +import org.schabi.newpipe.extractor.search.filter.FilterGroup; +import org.schabi.newpipe.extractor.search.filter.FilterItem; +import org.schabi.newpipe.extractor.search.filter.LibraryStringIds; +import org.schabi.newpipe.util.ServiceHelper; + +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.appcompat.view.menu.MenuBuilder; +import androidx.core.view.MenuCompat; + +import static android.content.ContentValues.TAG; +import static org.schabi.newpipe.fragments.list.search.filter.InjectFilterItem.DividerItem; +import static org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic.ICreateUiForFiltersWorker; +import static org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic.IUiItemWrapper; + +/** + * The implementation of the action menu based 'dialog'. + */ +public class SearchFilterUIOptionMenu extends BaseSearchFilterUiGenerator { + + // Menu groups identifier + private static final int MENU_GROUP_SEARCH_RESET_BUTTONS = 0; + // give them negative ids to not conflict with the ids of the filters + private static final int MENU_ID_SEARCH_BUTTON = -100; + private static final int MENU_ID_RESET_BUTTON = -101; + private Menu menu = null; + // initialize with first group id -> next group after the search/reset buttons group + private int newLastUsedGroupId = MENU_GROUP_SEARCH_RESET_BUTTONS + 1; + private int firstSortFilterGroupId; + + public SearchFilterUIOptionMenu( + @NonNull final SearchFilterLogic logic, + @NonNull final Context context) { + super(logic, context); + } + + int getLastUsedGroupIdThanIncrement() { + return newLastUsedGroupId++; + } + + @SuppressLint("RestrictedApi") + private void alwaysShowMenuItemIcon(final Menu theMenu) { + // always show icons + if (theMenu instanceof MenuBuilder) { + final MenuBuilder builder = ((MenuBuilder) theMenu); + builder.setOptionalIconsVisible(true); + } + } + + public void createSearchUI(@NonNull final Menu theMenu) { + this.menu = theMenu; + alwaysShowMenuItemIcon(theMenu); + + createSearchUI(); + + MenuCompat.setGroupDividerEnabled(theMenu, true); + } + + public boolean onOptionsItemSelected(@NonNull final MenuItem item) { + if (item.getGroupId() == MENU_GROUP_SEARCH_RESET_BUTTONS + && item.getItemId() == MENU_ID_SEARCH_BUTTON) { + logic.prepareForSearch(); + } else { // all other menu groups -> reset, content filters and sort filters + + // main part for holding onto the menu -> not closing it + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW); + item.setActionView(new View(context)); + item.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { + + @Override + public boolean onMenuItemActionExpand(final MenuItem item) { + if (item.getGroupId() == MENU_GROUP_SEARCH_RESET_BUTTONS + && item.getItemId() == MENU_ID_RESET_BUTTON) { + logic.reset(); + } else if (item.getGroupId() < firstSortFilterGroupId) { // content filters + final int filterId = item.getItemId(); + logic.selectContentFilter(filterId); + } else { // the sort filters + Log.d(TAG, "onMenuItemActionExpand: sort filters are here"); + logic.selectSortFilter(item.getItemId()); + } + + return false; + } + + @Override + public boolean onMenuItemActionCollapse(final MenuItem item) { + return false; + } + }); + } + + return false; + } + + @Override + protected ICreateUiForFiltersWorker createSortFilterWorker() { + return new CreateSortFilterUI(); + } + + @Override + protected ICreateUiForFiltersWorker createContentFilterWorker() { + return new CreateContentFilterUI(); + } + + private static class UiItemWrapper implements IUiItemWrapper { + + private final MenuItem item; + + UiItemWrapper(final MenuItem item) { + this.item = item; + } + + @Override + public void setVisible(final boolean visible) { + item.setVisible(visible); + } + + @Override + public int getItemId() { + return item.getItemId(); + } + + @Override + public boolean isChecked() { + return item.isChecked(); + } + + @Override + public void setChecked(final boolean checked) { + item.setChecked(checked); + } + } + + private class CreateContentFilterUI implements ICreateUiForFiltersWorker { + + /** + * MenuItem's that should not be checkable. + */ + final List nonCheckableMenuItems = new ArrayList<>(); + + /** + * {@link Menu#setGroupCheckable(int, boolean, boolean)} makes all {@link MenuItem} + * checkable. + *

+ * We do not want a group header or a group divider to be checkable. Therefore this method + * calls above mentioned method and afterwards makes all items uncheckable that are placed + * inside {@link #nonCheckableMenuItems}. + * + * @param isOnlyOneCheckable is in group only one selection allowed. + * @param groupId which group should be affected + */ + private void makeAllowedMenuItemInGroupCheckable(final boolean isOnlyOneCheckable, + final int groupId) { + // this method makes all MenuItem's checkable + menu.setGroupCheckable(groupId, true, isOnlyOneCheckable); + // uncheckable unwanted + for (final MenuItem uncheckableItem : nonCheckableMenuItems) { + if (uncheckableItem != null) { + uncheckableItem.setCheckable(false); + } + } + nonCheckableMenuItems.clear(); + } + + @Override + public void prepare() { + // create the search button + menu.add(MENU_GROUP_SEARCH_RESET_BUTTONS, + MENU_ID_SEARCH_BUTTON, + 0, + context.getString(R.string.search)) + .setEnabled(true) + .setCheckable(false) + .setIcon(R.drawable.ic_search); + + menu.add(MENU_GROUP_SEARCH_RESET_BUTTONS, + MENU_ID_RESET_BUTTON, + 0, + context.getString(R.string.playback_reset)) + .setEnabled(true) + .setCheckable(false) + .setIcon(R.drawable.ic_settings_backup_restore); + } + + @Override + public void createFilterGroupBeforeItems( + @NonNull final FilterGroup filterGroup) { + if (filterGroup.getNameId() != null) { + createNotEnabledAndUncheckableGroupTitleMenuItem( + FilterContainer.ITEM_IDENTIFIER_UNKNOWN, filterGroup.getNameId()); + } + } + + protected MenuItem createNotEnabledAndUncheckableGroupTitleMenuItem( + final int identifier, + final LibraryStringIds nameId) { + final MenuItem item = menu.add( + newLastUsedGroupId, + identifier, + 0, + ServiceHelper.getTranslatedFilterString(nameId, context)); + item.setEnabled(false); + + nonCheckableMenuItems.add(item); + + return item; + + } + + @Override + public void createFilterItem(@NonNull final FilterItem filterItem, + @NonNull final FilterGroup filterGroup) { + final MenuItem item = createMenuItem(filterItem); + + if (filterItem instanceof DividerItem) { + final DividerItem dividerItem = (DividerItem) filterItem; + final String menuDividerTitle = ">>>" + + context.getString(dividerItem.getStringResId()) + + "<<<"; + item.setTitle(menuDividerTitle); + item.setEnabled(false); + nonCheckableMenuItems.add(item); + } + + logic.addContentFilterUiWrapperToItemMap(filterItem.getIdentifier(), + new UiItemWrapper(item)); + } + + protected MenuItem createMenuItem(final FilterItem filterItem) { + return menu.add(newLastUsedGroupId, + filterItem.getIdentifier(), + 0, + ServiceHelper.getTranslatedFilterString(filterItem.getNameId(), context)); + } + + @Override + public void createFilterGroupAfterItems(@NonNull final FilterGroup filterGroup) { + makeAllowedMenuItemInGroupCheckable(filterGroup.isOnlyOneCheckable(), + getLastUsedGroupIdThanIncrement()); + } + + @Override + public void finish() { + firstSortFilterGroupId = newLastUsedGroupId; + } + + @Override + public void filtersVisible(final boolean areFiltersVisible) { + // no implementation here as there is no 'sort filter' title as MenuItem + } + } + + private class CreateSortFilterUI extends CreateContentFilterUI { + + private void addSortFilterUiToItemMap(final int id, + final MenuItem item) { + logic.addSortFilterUiWrapperToItemMap(id, new UiItemWrapper(item)); + } + + @Override + public void prepare() { + firstSortFilterGroupId = newLastUsedGroupId; + } + + @Override + public void createFilterGroupBeforeItems( + @NonNull final FilterGroup filterGroup) { + if (filterGroup.getNameId() != null) { + final MenuItem item = createNotEnabledAndUncheckableGroupTitleMenuItem( + filterGroup.getIdentifier(), filterGroup.getNameId()); + addSortFilterUiToItemMap(filterGroup.getIdentifier(), item); + } + } + + @Override + public void createFilterItem(@NonNull final FilterItem filterItem, + @NonNull final FilterGroup filterGroup) { + final MenuItem item = createMenuItem(filterItem); + addSortFilterUiToItemMap(filterItem.getIdentifier(), item); + } + + @Override + public void finish() { + // no implementation here as we do not need to clean up anything or whatever + } + } +}