mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-07-23 06:09:50 -06:00
Android: Fix CheatsActivity d-pad navigation
Special shoutout to Android for not having RTL compatible variants of nextFocusRight and nextFocusLeft. Ideally we would have some way to block the user from using the d-pad to switch between the two panes when in portrait mode, or make the list pane act as if it's to the left of the details pane rather than the right when the details pane is open, but I don't know of a good way to do this. SlidingPaneLayout doesn't really seem to have been implemented with d-pad navigation in mind. Thankfully, landscape is the most important use case for gamepads.
This commit is contained in:
@ -73,13 +73,12 @@ public class CheatDetailsFragment extends Fragment
|
|||||||
mViewModel.getIsEditing().observe(getViewLifecycleOwner(), this::onIsEditingUpdated);
|
mViewModel.getIsEditing().observe(getViewLifecycleOwner(), this::onIsEditingUpdated);
|
||||||
|
|
||||||
mButtonDelete.setOnClickListener(this::onDeleteClicked);
|
mButtonDelete.setOnClickListener(this::onDeleteClicked);
|
||||||
mButtonEdit.setOnClickListener((v) -> mViewModel.setIsEditing(true));
|
mButtonEdit.setOnClickListener(this::onEditClicked);
|
||||||
mButtonCancel.setOnClickListener((v) ->
|
mButtonCancel.setOnClickListener(this::onCancelClicked);
|
||||||
{
|
|
||||||
mViewModel.setIsEditing(false);
|
|
||||||
onSelectedCheatUpdated(mCheat);
|
|
||||||
});
|
|
||||||
mButtonOk.setOnClickListener(this::onOkClicked);
|
mButtonOk.setOnClickListener(this::onOkClicked);
|
||||||
|
|
||||||
|
CheatsActivity.setOnFocusChangeListenerRecursively(view,
|
||||||
|
(v, hasFocus) -> activity.onDetailsViewFocusChange(hasFocus));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearEditErrors()
|
private void clearEditErrors()
|
||||||
@ -98,6 +97,19 @@ public class CheatDetailsFragment extends Fragment
|
|||||||
builder.show();
|
builder.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onEditClicked(View view)
|
||||||
|
{
|
||||||
|
mViewModel.setIsEditing(true);
|
||||||
|
mButtonOk.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onCancelClicked(View view)
|
||||||
|
{
|
||||||
|
mViewModel.setIsEditing(false);
|
||||||
|
onSelectedCheatUpdated(mCheat);
|
||||||
|
mButtonDelete.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
private void onOkClicked(View view)
|
private void onOkClicked(View view)
|
||||||
{
|
{
|
||||||
clearEditErrors();
|
clearEditErrors();
|
||||||
@ -118,6 +130,7 @@ public class CheatDetailsFragment extends Fragment
|
|||||||
mViewModel.notifySelectedCheatChanged();
|
mViewModel.notifySelectedCheatChanged();
|
||||||
mViewModel.setIsEditing(false);
|
mViewModel.setIsEditing(false);
|
||||||
}
|
}
|
||||||
|
mButtonEdit.requestFocus();
|
||||||
break;
|
break;
|
||||||
case Cheat.TRY_SET_FAIL_NO_NAME:
|
case Cheat.TRY_SET_FAIL_NO_NAME:
|
||||||
mEditName.setError(getString(R.string.cheats_error_no_name));
|
mEditName.setError(getString(R.string.cheats_error_no_name));
|
||||||
|
@ -36,7 +36,7 @@ public class CheatListFragment extends Fragment
|
|||||||
CheatsActivity activity = (CheatsActivity) requireActivity();
|
CheatsActivity activity = (CheatsActivity) requireActivity();
|
||||||
CheatsViewModel viewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
|
CheatsViewModel viewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
|
||||||
|
|
||||||
recyclerView.setAdapter(new CheatsAdapter(getViewLifecycleOwner(), viewModel));
|
recyclerView.setAdapter(new CheatsAdapter(activity, viewModel));
|
||||||
recyclerView.setLayoutManager(new LinearLayoutManager(activity));
|
recyclerView.setLayoutManager(new LinearLayoutManager(activity));
|
||||||
recyclerView.addItemDecoration(new DividerItemDecoration(activity, null));
|
recyclerView.addItemDecoration(new DividerItemDecoration(activity, null));
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,10 @@ public class CheatWarningFragment extends Fragment implements View.OnClickListen
|
|||||||
|
|
||||||
Button settingsButton = view.findViewById(R.id.button_settings);
|
Button settingsButton = view.findViewById(R.id.button_settings);
|
||||||
settingsButton.setOnClickListener(this);
|
settingsButton.setOnClickListener(this);
|
||||||
|
|
||||||
|
CheatsActivity activity = (CheatsActivity) requireActivity();
|
||||||
|
CheatsActivity.setOnFocusChangeListenerRecursively(view,
|
||||||
|
(v, hasFocus) -> activity.onListViewFocusChange(hasFocus));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -5,8 +5,12 @@ package org.dolphinemu.dolphinemu.features.cheats.ui;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.slidingpanelayout.widget.SlidingPaneLayout;
|
import androidx.slidingpanelayout.widget.SlidingPaneLayout;
|
||||||
|
|
||||||
@ -18,6 +22,7 @@ import org.dolphinemu.dolphinemu.ui.TwoPaneOnBackPressedCallback;
|
|||||||
import org.dolphinemu.dolphinemu.ui.main.MainPresenter;
|
import org.dolphinemu.dolphinemu.ui.main.MainPresenter;
|
||||||
|
|
||||||
public class CheatsActivity extends AppCompatActivity
|
public class CheatsActivity extends AppCompatActivity
|
||||||
|
implements SlidingPaneLayout.PanelSlideListener
|
||||||
{
|
{
|
||||||
private static final String ARG_GAME_ID = "game_id";
|
private static final String ARG_GAME_ID = "game_id";
|
||||||
private static final String ARG_REVISION = "revision";
|
private static final String ARG_REVISION = "revision";
|
||||||
@ -29,6 +34,11 @@ public class CheatsActivity extends AppCompatActivity
|
|||||||
private CheatsViewModel mViewModel;
|
private CheatsViewModel mViewModel;
|
||||||
|
|
||||||
private SlidingPaneLayout mSlidingPaneLayout;
|
private SlidingPaneLayout mSlidingPaneLayout;
|
||||||
|
private View mCheatList;
|
||||||
|
private View mCheatDetails;
|
||||||
|
|
||||||
|
private View mCheatListLastFocus;
|
||||||
|
private View mCheatDetailsLastFocus;
|
||||||
|
|
||||||
public static void launch(Context context, String gameId, int revision, boolean isWii)
|
public static void launch(Context context, String gameId, int revision, boolean isWii)
|
||||||
{
|
{
|
||||||
@ -59,6 +69,13 @@ public class CheatsActivity extends AppCompatActivity
|
|||||||
setContentView(R.layout.activity_cheats);
|
setContentView(R.layout.activity_cheats);
|
||||||
|
|
||||||
mSlidingPaneLayout = findViewById(R.id.sliding_pane_layout);
|
mSlidingPaneLayout = findViewById(R.id.sliding_pane_layout);
|
||||||
|
mCheatList = findViewById(R.id.cheat_list);
|
||||||
|
mCheatDetails = findViewById(R.id.cheat_details);
|
||||||
|
|
||||||
|
mCheatListLastFocus = mCheatList;
|
||||||
|
mCheatDetailsLastFocus = mCheatDetails;
|
||||||
|
|
||||||
|
mSlidingPaneLayout.addPanelSlideListener(this);
|
||||||
|
|
||||||
getOnBackPressedDispatcher().addCallback(this,
|
getOnBackPressedDispatcher().addCallback(this,
|
||||||
new TwoPaneOnBackPressedCallback(mSlidingPaneLayout));
|
new TwoPaneOnBackPressedCallback(mSlidingPaneLayout));
|
||||||
@ -77,6 +94,25 @@ public class CheatsActivity extends AppCompatActivity
|
|||||||
mViewModel.saveIfNeeded(mGameId, mRevision);
|
mViewModel.saveIfNeeded(mGameId, mRevision);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPanelSlide(@NonNull View panel, float slideOffset)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPanelOpened(@NonNull View panel)
|
||||||
|
{
|
||||||
|
boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL;
|
||||||
|
mCheatDetailsLastFocus.requestFocus(rtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPanelClosed(@NonNull View panel)
|
||||||
|
{
|
||||||
|
boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL;
|
||||||
|
mCheatListLastFocus.requestFocus(rtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
private void onSelectedCheatChanged(Cheat selectedCheat)
|
private void onSelectedCheatChanged(Cheat selectedCheat)
|
||||||
{
|
{
|
||||||
boolean cheatSelected = selectedCheat != null;
|
boolean cheatSelected = selectedCheat != null;
|
||||||
@ -88,6 +124,30 @@ public class CheatsActivity extends AppCompatActivity
|
|||||||
SlidingPaneLayout.LOCK_MODE_UNLOCKED : SlidingPaneLayout.LOCK_MODE_LOCKED_CLOSED);
|
SlidingPaneLayout.LOCK_MODE_UNLOCKED : SlidingPaneLayout.LOCK_MODE_LOCKED_CLOSED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onListViewFocusChange(boolean hasFocus)
|
||||||
|
{
|
||||||
|
if (hasFocus)
|
||||||
|
{
|
||||||
|
mCheatListLastFocus = mCheatList.findFocus();
|
||||||
|
if (mCheatListLastFocus == null)
|
||||||
|
throw new NullPointerException();
|
||||||
|
|
||||||
|
mSlidingPaneLayout.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onDetailsViewFocusChange(boolean hasFocus)
|
||||||
|
{
|
||||||
|
if (hasFocus)
|
||||||
|
{
|
||||||
|
mCheatDetailsLastFocus = mCheatDetails.findFocus();
|
||||||
|
if (mCheatDetailsLastFocus == null)
|
||||||
|
throw new NullPointerException();
|
||||||
|
|
||||||
|
mSlidingPaneLayout.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void openDetailsView(boolean open)
|
private void openDetailsView(boolean open)
|
||||||
{
|
{
|
||||||
if (open)
|
if (open)
|
||||||
@ -100,4 +160,20 @@ public class CheatsActivity extends AppCompatActivity
|
|||||||
settings.loadSettings(null, mGameId, mRevision, mIsWii);
|
settings.loadSettings(null, mGameId, mRevision, mIsWii);
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void setOnFocusChangeListenerRecursively(@NonNull View view,
|
||||||
|
View.OnFocusChangeListener listener)
|
||||||
|
{
|
||||||
|
view.setOnFocusChangeListener(listener);
|
||||||
|
|
||||||
|
if (view instanceof ViewGroup)
|
||||||
|
{
|
||||||
|
ViewGroup viewGroup = (ViewGroup) view;
|
||||||
|
for (int i = 0; i < viewGroup.getChildCount(); i++)
|
||||||
|
{
|
||||||
|
View child = viewGroup.getChildAt(i);
|
||||||
|
setOnFocusChangeListenerRecursively(child, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,25 +20,27 @@ import java.util.ArrayList;
|
|||||||
|
|
||||||
public class CheatsAdapter extends RecyclerView.Adapter<CheatItemViewHolder>
|
public class CheatsAdapter extends RecyclerView.Adapter<CheatItemViewHolder>
|
||||||
{
|
{
|
||||||
|
private final CheatsActivity mActivity;
|
||||||
private final CheatsViewModel mViewModel;
|
private final CheatsViewModel mViewModel;
|
||||||
|
|
||||||
public CheatsAdapter(LifecycleOwner owner, CheatsViewModel viewModel)
|
public CheatsAdapter(CheatsActivity activity, CheatsViewModel viewModel)
|
||||||
{
|
{
|
||||||
|
mActivity = activity;
|
||||||
mViewModel = viewModel;
|
mViewModel = viewModel;
|
||||||
|
|
||||||
mViewModel.getCheatAddedEvent().observe(owner, (position) ->
|
mViewModel.getCheatAddedEvent().observe(activity, (position) ->
|
||||||
{
|
{
|
||||||
if (position != null)
|
if (position != null)
|
||||||
notifyItemInserted(position);
|
notifyItemInserted(position);
|
||||||
});
|
});
|
||||||
|
|
||||||
mViewModel.getCheatChangedEvent().observe(owner, (position) ->
|
mViewModel.getCheatChangedEvent().observe(activity, (position) ->
|
||||||
{
|
{
|
||||||
if (position != null)
|
if (position != null)
|
||||||
notifyItemChanged(position);
|
notifyItemChanged(position);
|
||||||
});
|
});
|
||||||
|
|
||||||
mViewModel.getCheatDeletedEvent().observe(owner, (position) ->
|
mViewModel.getCheatDeletedEvent().observe(activity, (position) ->
|
||||||
{
|
{
|
||||||
if (position != null)
|
if (position != null)
|
||||||
notifyItemRemoved(position);
|
notifyItemRemoved(position);
|
||||||
@ -55,12 +57,15 @@ public class CheatsAdapter extends RecyclerView.Adapter<CheatItemViewHolder>
|
|||||||
{
|
{
|
||||||
case CheatItem.TYPE_CHEAT:
|
case CheatItem.TYPE_CHEAT:
|
||||||
View cheatView = inflater.inflate(R.layout.list_item_cheat, parent, false);
|
View cheatView = inflater.inflate(R.layout.list_item_cheat, parent, false);
|
||||||
|
addViewListeners(cheatView);
|
||||||
return new CheatViewHolder(cheatView);
|
return new CheatViewHolder(cheatView);
|
||||||
case CheatItem.TYPE_HEADER:
|
case CheatItem.TYPE_HEADER:
|
||||||
View headerView = inflater.inflate(R.layout.list_item_header, parent, false);
|
View headerView = inflater.inflate(R.layout.list_item_header, parent, false);
|
||||||
|
addViewListeners(headerView);
|
||||||
return new HeaderViewHolder(headerView);
|
return new HeaderViewHolder(headerView);
|
||||||
case CheatItem.TYPE_ACTION:
|
case CheatItem.TYPE_ACTION:
|
||||||
View actionView = inflater.inflate(R.layout.list_item_submenu, parent, false);
|
View actionView = inflater.inflate(R.layout.list_item_submenu, parent, false);
|
||||||
|
addViewListeners(actionView);
|
||||||
return new ActionViewHolder(actionView);
|
return new ActionViewHolder(actionView);
|
||||||
default:
|
default:
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
@ -86,6 +91,12 @@ public class CheatsAdapter extends RecyclerView.Adapter<CheatItemViewHolder>
|
|||||||
return getItemAt(position).getType();
|
return getItemAt(position).getType();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addViewListeners(View view)
|
||||||
|
{
|
||||||
|
CheatsActivity.setOnFocusChangeListenerRecursively(view,
|
||||||
|
(v, hasFocus) -> mActivity.onListViewFocusChange(hasFocus));
|
||||||
|
}
|
||||||
|
|
||||||
private CheatItem getItemAt(int position)
|
private CheatItem getItemAt(int position)
|
||||||
{
|
{
|
||||||
// Patches
|
// Patches
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/root"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:focusable="true"
|
||||||
|
android:nextFocusLeft="@id/checkbox">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
style="@style/TextAppearance.AppCompat.Headline"
|
||||||
|
android:textSize="16sp"
|
||||||
|
tools:text="Hyrule Field Speed Hack"
|
||||||
|
android:layout_margin="@dimen/spacing_large"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/checkbox"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/checkbox"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/text_name"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:focusable="true"
|
||||||
|
android:nextFocusRight="@id/root" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -6,7 +6,8 @@
|
|||||||
android:id="@+id/root"
|
android:id="@+id/root"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:focusable="true">
|
android:focusable="true"
|
||||||
|
android:nextFocusRight="@id/checkbox">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_name"
|
android:id="@+id/text_name"
|
||||||
@ -30,6 +31,7 @@
|
|||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:focusable="true" />
|
android:focusable="true"
|
||||||
|
android:nextFocusLeft="@id/root" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
Reference in New Issue
Block a user