From c849af47da450a3ef3bc5304587115eac08f468f Mon Sep 17 00:00:00 2001 From: Gabriel Machado <97042217+GabrielBRDeveloper@users.noreply.github.com> Date: Wed, 3 Jan 2024 17:35:30 -0400 Subject: [PATCH] Pandroid: add option to change screen gamepad, buttons position. (#359) * Add change screen gamepad: initial commit * Add change gamepad positions: part 2 * Peach fixes * Bonk --------- Co-authored-by: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> --- .../pandroid/app/PreferenceActivity.java | 9 +- .../pandroid/app/base/BottomAlertDialog.java | 57 +++++ .../pandroid/app/main/SettingsFragment.java | 4 +- .../preferences/AppearancePreferences.java | 1 - .../ControllerMapperPreferences.java | 101 +++++++++ .../app/preferences/InputPreferences.java | 109 ++++++++++ .../pandroid/data/GsonConfigParser.java | 3 +- .../panda3ds/pandroid/utils/Constants.java | 1 + .../pandroid/view/PandaLayoutController.java | 33 +++ .../controller/mapping/ControllerItem.java | 9 + .../controller/mapping/ControllerMapper.java | 202 ++++++++++++++++++ .../mapping/ControllerProfileManager.java | 103 +++++++++ .../view/controller/mapping/Layout.java | 36 ++++ .../view/controller/mapping/Location.java | 57 +++++ .../view/controller/mapping/Profile.java | 104 +++++++++ .../app/src/main/res/color/red_color.xml | 4 + .../res/drawable/alert_dialog_background.xml | 9 + .../app/src/main/res/drawable/ic_add.xml | 2 +- .../app/src/main/res/drawable/ic_delete.xml | 5 + .../main/res/drawable/ic_rotate_screen.xml | 5 + .../app/src/main/res/drawable/ic_save.xml | 5 + .../src/main/res/drawable/ic_visibility.xml | 5 + .../src/main/res/layout/controller_dpad.xml | 1 + .../main/res/layout/controller_gamepad.xml | 4 +- .../app/src/main/res/layout/game_activity.xml | 3 +- .../layout/preference_controller_mapper.xml | 90 ++++++++ .../src/main/res/values-pt-rBR/strings.xml | 8 + .../app/src/main/res/values/dimens.xml | 4 + .../app/src/main/res/values/strings.xml | 10 +- .../app/src/main/res/values/themes.xml | 21 +- .../app/src/main/res/xml/input_preference.xml | 31 +++ .../src/main/res/xml/start_preferences.xml | 6 +- 32 files changed, 1028 insertions(+), 14 deletions(-) create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BottomAlertDialog.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/ControllerMapperPreferences.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputPreferences.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerItem.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerMapper.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerProfileManager.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Layout.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Location.java create mode 100644 src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Profile.java create mode 100644 src/pandroid/app/src/main/res/color/red_color.xml create mode 100644 src/pandroid/app/src/main/res/drawable/alert_dialog_background.xml create mode 100644 src/pandroid/app/src/main/res/drawable/ic_delete.xml create mode 100644 src/pandroid/app/src/main/res/drawable/ic_rotate_screen.xml create mode 100644 src/pandroid/app/src/main/res/drawable/ic_save.xml create mode 100644 src/pandroid/app/src/main/res/drawable/ic_visibility.xml create mode 100644 src/pandroid/app/src/main/res/layout/preference_controller_mapper.xml create mode 100644 src/pandroid/app/src/main/res/values/dimens.xml create mode 100644 src/pandroid/app/src/main/res/xml/input_preference.xml diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PreferenceActivity.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PreferenceActivity.java index 64249c5b5..9bc9900b7 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PreferenceActivity.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/PreferenceActivity.java @@ -29,15 +29,22 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { try { Class clazz = getClassLoader().loadClass(intent.getStringExtra(Constants.ACTIVITY_PARAMETER_FRAGMENT)); - getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, (Fragment) clazz.newInstance()).commitNow(); + Fragment fragment = (Fragment) clazz.newInstance(); + fragment.setArguments(intent.getExtras()); + getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, fragment).commitNow(); } catch (Exception e) { throw new RuntimeException(e); } } public static void launch(Context context, Class clazz) { + launch(context, clazz, new Intent()); + } + + public static void launch(Context context, Class clazz, Intent extras) { context.startActivity(new Intent(context, PreferenceActivity.class) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtras(extras) .putExtra(Constants.ACTIVITY_PARAMETER_FRAGMENT, clazz.getName())); } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BottomAlertDialog.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BottomAlertDialog.java new file mode 100644 index 000000000..7c5470f1d --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/base/BottomAlertDialog.java @@ -0,0 +1,57 @@ +package com.panda3ds.pandroid.app.base; + +import android.content.Context; +import android.view.Gravity; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.widget.AppCompatEditText; +import androidx.appcompat.widget.LinearLayoutCompat; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.lang.Function; + +public class BottomAlertDialog extends AlertDialog.Builder { + private final LinearLayoutCompat layoutCompat; + + public BottomAlertDialog(@NonNull Context context) { + super(context, R.style.AlertDialog); + layoutCompat = new LinearLayoutCompat(context); + layoutCompat.setOrientation(LinearLayoutCompat.VERTICAL); + + int padding = getContext().getResources().getDimensionPixelSize(androidx.appcompat.R.dimen.abc_dialog_padding_material); + layoutCompat.setPadding(padding, 0, padding, 0); + + setView(layoutCompat); + } + + @NonNull + @Override + public AlertDialog create() { + AlertDialog dialog = super.create(); + dialog.getWindow().setGravity(Gravity.BOTTOM | Gravity.CENTER); + dialog.getWindow().getAttributes().y = Math.round(getContext().getResources().getDisplayMetrics().density * 15); + return dialog; + } + + public BottomAlertDialog setTextInput(String hint, Function listener) { + AppCompatEditText edit = new AppCompatEditText(getContext()); + edit.setHint(hint); + int margin = layoutCompat.getPaddingLeft() / 2; + LinearLayoutCompat.LayoutParams params = new LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + params.setMargins(0, margin, 0, margin); + layoutCompat.addView(edit, params); + setPositiveButton(android.R.string.ok, (dialog, which) -> listener.run(String.valueOf(edit.getText()))); + setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss()); + return this; + } + + @Override + public AlertDialog show() { + AlertDialog dialog = create(); + dialog.show(); + + return dialog; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SettingsFragment.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SettingsFragment.java index 08f2d70f8..b3bebf8fc 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SettingsFragment.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/main/SettingsFragment.java @@ -7,14 +7,14 @@ import com.panda3ds.pandroid.R; import com.panda3ds.pandroid.app.PreferenceActivity; import com.panda3ds.pandroid.app.base.BasePreferenceFragment; -import com.panda3ds.pandroid.app.preferences.InputMapPreferences; import com.panda3ds.pandroid.app.preferences.AppearancePreferences; +import com.panda3ds.pandroid.app.preferences.InputPreferences; public class SettingsFragment extends BasePreferenceFragment { @Override public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { setPreferencesFromResource(R.xml.start_preferences, rootKey); - setItemClick("inputMap", (item) -> PreferenceActivity.launch(requireContext(), InputMapPreferences.class)); + setItemClick("input", (item) -> PreferenceActivity.launch(requireContext(), InputPreferences.class)); setItemClick("appearance", (item)-> PreferenceActivity.launch(requireContext(), AppearancePreferences.class)); } } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AppearancePreferences.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AppearancePreferences.java index 63f79ddb4..dea4e2613 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AppearancePreferences.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/AppearancePreferences.java @@ -1,6 +1,5 @@ package com.panda3ds.pandroid.app.preferences; -import android.app.Activity; import android.os.Bundle; import androidx.annotation.Nullable; diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/ControllerMapperPreferences.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/ControllerMapperPreferences.java new file mode 100644 index 000000000..e59adfbea --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/ControllerMapperPreferences.java @@ -0,0 +1,101 @@ +package com.panda3ds.pandroid.app.preferences; + +import android.content.pm.ActivityInfo; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.BaseActivity; +import com.panda3ds.pandroid.app.base.BottomAlertDialog; +import com.panda3ds.pandroid.view.controller.mapping.ControllerMapper; +import com.panda3ds.pandroid.view.controller.mapping.ControllerProfileManager; +import com.panda3ds.pandroid.view.controller.mapping.ControllerItem; +import com.panda3ds.pandroid.view.controller.mapping.Profile; + +public class ControllerMapperPreferences extends Fragment { + private Profile currentProfile; + private ControllerMapper mapper; + private View saveButton; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.preference_controller_mapper, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + + currentProfile = ControllerProfileManager.get(getArguments().getString("profile")).clone(); + + ((BaseActivity) requireActivity()).getSupportActionBar().hide(); + mapper = view.findViewById(R.id.mapper); + mapper.initialize(this::onLocationChanged, currentProfile); + + view.findViewById(R.id.change_visibility).setOnClickListener(v -> { + BottomAlertDialog builder = new BottomAlertDialog(v.getContext()); + builder.setTitle("Visibility"); + boolean[] visibleList = { + currentProfile.isVisible(ControllerItem.START), + currentProfile.isVisible(ControllerItem.SELECT), + currentProfile.isVisible(ControllerItem.L), + currentProfile.isVisible(ControllerItem.R), + currentProfile.isVisible(ControllerItem.DPAD), + currentProfile.isVisible(ControllerItem.JOYSTICK), + currentProfile.isVisible(ControllerItem.GAMEPAD), + }; + builder.setMultiChoiceItems(new CharSequence[]{ + "Start", "Select", "L", "R", "Dpad", getString(R.string.axis), "A/B/X/Y" + }, visibleList, (dialog, index, visibility) -> { + visibleList[index] = visibility; + }).setPositiveButton(android.R.string.ok, (dialog, which) -> { + + saveButton.setVisibility(View.VISIBLE); + + currentProfile.setVisible(ControllerItem.START, visibleList[0]); + currentProfile.setVisible(ControllerItem.SELECT, visibleList[1]); + currentProfile.setVisible(ControllerItem.L, visibleList[2]); + currentProfile.setVisible(ControllerItem.R, visibleList[3]); + currentProfile.setVisible(ControllerItem.DPAD, visibleList[4]); + currentProfile.setVisible(ControllerItem.JOYSTICK, visibleList[5]); + currentProfile.setVisible(ControllerItem.GAMEPAD, visibleList[6]); + + mapper.refreshLayout(); + }).setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss()); + builder.show(); + }); + + saveButton = view.findViewById(R.id.save); + saveButton.setOnClickListener(v -> { + ControllerProfileManager.add(currentProfile); + Toast.makeText(v.getContext(), R.string.saved, Toast.LENGTH_SHORT).show(); + requireActivity().finish(); + }); + + view.findViewById(R.id.delete).setOnClickListener(v -> { + ControllerProfileManager.remove(currentProfile.getId()); + requireActivity().finish(); + }); + + view.findViewById(R.id.rotate).setOnClickListener(v -> { + requireActivity().setRequestedOrientation(mapper.getCurrentWidth() > mapper.getCurrentHeight() ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT : ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); + }); + + view.findViewById(R.id.delete).setVisibility(ControllerProfileManager.getProfileCount() > 1 ? View.VISIBLE : View.GONE); + + saveButton.setVisibility(View.GONE); + } + + public void onLocationChanged(ControllerItem id) { + saveButton.setVisibility(View.VISIBLE); + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputPreferences.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputPreferences.java new file mode 100644 index 000000000..aa5d5eff7 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/app/preferences/InputPreferences.java @@ -0,0 +1,109 @@ +package com.panda3ds.pandroid.app.preferences; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.os.Bundle; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.BaseActivity; +import com.panda3ds.pandroid.app.PreferenceActivity; +import com.panda3ds.pandroid.app.base.BasePreferenceFragment; +import com.panda3ds.pandroid.app.base.BottomAlertDialog; +import com.panda3ds.pandroid.view.controller.mapping.ControllerProfileManager; +import com.panda3ds.pandroid.view.controller.mapping.Profile; + +import java.util.List; +import java.util.Objects; + +public class InputPreferences extends BasePreferenceFragment { + + public static final String ID_DEFAULT_CONTROLLER_PROFILE = "defaultControllerProfile"; + public static final String ID_INPUT_MAP = "inputMap"; + public static final String ID_CREATE_PROFILE = "createProfile"; + private static final CharSequence ID_GAMEPAD_PROFILE_LIST = "gamepadProfileList"; + + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { + setPreferencesFromResource(R.xml.input_preference, rootKey); + setItemClick(ID_INPUT_MAP, (item) -> PreferenceActivity.launch(requireContext(), InputMapPreferences.class)); + setItemClick(ID_CREATE_PROFILE, (item) -> { + new BottomAlertDialog(requireContext()) + .setTextInput(getString(R.string.name), (name) -> { + name = formatName(name); + if (name.length() > 0) { + Profile profile = ControllerProfileManager.makeDefaultProfile(); + profile.setName(name); + ControllerProfileManager.add(profile); + refreshScreenProfileList(); + } else { + Toast.makeText(requireContext(), R.string.invalid_name, Toast.LENGTH_SHORT).show(); + } + }).setTitle(R.string.create_profile).show(); + }); + + setItemClick(ID_DEFAULT_CONTROLLER_PROFILE, (item) -> { + List profiles = ControllerProfileManager.listAll(); + String defaultProfileId = ControllerProfileManager.getDefaultProfile().getId(); + int defaultProfileIndex = 0; + CharSequence[] names = new CharSequence[profiles.size()]; + for (int i = 0; i < names.length; i++) { + names[i] = profiles.get(i).getName(); + if (Objects.equals(profiles.get(i).getId(), defaultProfileId)) { + defaultProfileIndex = i; + } + } + new BottomAlertDialog(item.getContext()) + .setSingleChoiceItems(names, defaultProfileIndex, (dialog, which) -> { + dialog.dismiss(); + ControllerProfileManager.setDefaultProfileId(profiles.get(which).getId()); + item.setSummary(profiles.get(which).getName()); + }).setTitle(R.string.pref_default_controller_title).show(); + }); + + ((BaseActivity) requireActivity()).getSupportActionBar().setTitle(R.string.input); + } + + public String formatName(String name) { + return name.trim().replaceAll("\\s\\s", " "); + } + + private void refresh() { + findPreference(ID_DEFAULT_CONTROLLER_PROFILE).setSummary(ControllerProfileManager.getDefaultProfile().getName()); + refreshScreenProfileList(); + } + + @SuppressLint("RestrictedApi") + private void refreshScreenProfileList() { + PreferenceCategory category = findPreference(ID_GAMEPAD_PROFILE_LIST); + Preference add = category.getPreference(category.getPreferenceCount() - 1); + category.removeAll(); + category.setOrderingAsAdded(true); + + for (Profile profile : ControllerProfileManager.listAll()) { + Preference item = new Preference(category.getContext()); + item.setOnPreferenceClickListener(preference -> { + category.performClick(); + PreferenceActivity.launch(requireActivity(), ControllerMapperPreferences.class, new Intent().putExtra("profile", profile.getId())); + return false; + }); + item.setOrder(category.getPreferenceCount()); + item.setIconSpaceReserved(false); + item.setTitle(profile.getName()); + category.addPreference(item); + } + + add.setOrder(category.getPreferenceCount()); + category.addPreference(add); + } + + @Override + public void onResume() { + super.onResume(); + refresh(); + } +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/GsonConfigParser.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/GsonConfigParser.java index 99c1419b4..0fde3d2f5 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/GsonConfigParser.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/GsonConfigParser.java @@ -1,11 +1,12 @@ package com.panda3ds.pandroid.data; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.panda3ds.pandroid.lang.Task; import com.panda3ds.pandroid.utils.FileUtils; public class GsonConfigParser { - private final Gson gson = new Gson(); + private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); private final String name; public GsonConfigParser(String name) { diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/Constants.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/Constants.java index 7adf2e479..c72a516a2 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/Constants.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/utils/Constants.java @@ -25,4 +25,5 @@ public class Constants { public static final String PREF_GLOBAL_CONFIG = "app.GlobalConfig"; public static final String PREF_GAME_UTILS = "app.GameUtils"; public static final String PREF_INPUT_MAP = "app.InputMap"; + public static final String PREF_SCREEN_CONTROLLER_PROFILES = "app.input.ScreenControllerManager"; } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaLayoutController.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaLayoutController.java index 617b407e9..2f341c7b2 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaLayoutController.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/PandaLayoutController.java @@ -6,10 +6,17 @@ import com.panda3ds.pandroid.R; import com.panda3ds.pandroid.utils.Constants; import com.panda3ds.pandroid.view.controller.ControllerLayout; +import com.panda3ds.pandroid.view.controller.mapping.ControllerProfileManager; +import com.panda3ds.pandroid.view.controller.mapping.ControllerItem; +import com.panda3ds.pandroid.view.controller.mapping.Profile; import com.panda3ds.pandroid.view.controller.nodes.Button; import com.panda3ds.pandroid.view.controller.nodes.Joystick; public class PandaLayoutController extends ControllerLayout { + + private int width = -1; + private int height = -1; + public PandaLayoutController(Context context) { super(context); } public PandaLayoutController(Context context, AttributeSet attrs) { super(context, attrs); } public PandaLayoutController(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @@ -44,5 +51,31 @@ public void initialize() { }); refreshChildren(); + measure(MeasureSpec.EXACTLY, MeasureSpec.EXACTLY); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int measuredWidth = getMeasuredWidth(); + int measuredHeight = getMeasuredHeight(); + + if (measuredWidth != width || measuredHeight != height) { + width = measuredWidth; + height = measuredHeight; + applyProfileMap(); + } + } + + private void applyProfileMap() { + Profile profile = ControllerProfileManager.getDefaultProfile(); + + profile.applyToView(ControllerItem.L,findViewById(R.id.button_l), width, height); + profile.applyToView(ControllerItem.R, findViewById(R.id.button_r), width, height); + profile.applyToView(ControllerItem.START, findViewById(R.id.button_start), width, height); + profile.applyToView(ControllerItem.SELECT, findViewById(R.id.button_select), width, height); + profile.applyToView(ControllerItem.JOYSTICK, findViewById(R.id.left_analog), width, height); + profile.applyToView(ControllerItem.GAMEPAD, findViewById(R.id.gamepad), width, height); + profile.applyToView(ControllerItem.DPAD, findViewById(R.id.dpad), width, height); } } diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerItem.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerItem.java new file mode 100644 index 000000000..6be617b50 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerItem.java @@ -0,0 +1,9 @@ +package com.panda3ds.pandroid.view.controller.mapping; + +public enum ControllerItem { + START, + SELECT, + L,R, + GAMEPAD, + DPAD, JOYSTICK +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerMapper.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerMapper.java new file mode 100644 index 000000000..04cb75c8c --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerMapper.java @@ -0,0 +1,202 @@ +package com.panda3ds.pandroid.view.controller.mapping; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.drawable.ColorDrawable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.lang.Function; +import com.panda3ds.pandroid.math.Vector2; + +public class ControllerMapper extends FrameLayout { + public static int COLOR_DARK = Color.rgb(20, 20, 20); + public static int COLOR_LIGHT = Color.rgb(60, 60, 60); + private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + private Profile profile; + private View selectedView; + private final Paint selectionPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private int width = -1; + private int height = -1; + private Function changeListener; + + public ControllerMapper(@NonNull Context context) { + this(context, null); + } + + public ControllerMapper(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public ControllerMapper(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public ControllerMapper(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + setBackground(new ColorDrawable(Color.YELLOW)); + float dp = getResources().getDisplayMetrics().density; + + selectionPaint.setColor(Color.RED); + selectionPaint.setStrokeWidth(dp * 2); + selectionPaint.setStyle(Paint.Style.STROKE); + selectionPaint.setPathEffect(new DashPathEffect(new float[]{dp * 10, dp * 10}, 0.0f)); + } + + public void initialize(Function changeListener, Profile profile) { + this.profile = profile; + this.changeListener = changeListener; + + measure(MeasureSpec.EXACTLY, MeasureSpec.EXACTLY); + + new MoveElementListener(ControllerItem.L, findViewById(R.id.button_l)); + new MoveElementListener(ControllerItem.R, findViewById(R.id.button_r)); + new MoveElementListener(ControllerItem.START, findViewById(R.id.button_start)); + new MoveElementListener(ControllerItem.SELECT, findViewById(R.id.button_select)); + new MoveElementListener(ControllerItem.DPAD, findViewById(R.id.dpad)); + new MoveElementListener(ControllerItem.GAMEPAD, findViewById(R.id.gamepad)); + new MoveElementListener(ControllerItem.JOYSTICK, findViewById(R.id.left_analog)); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + drawBackground(canvas); + if (selectedView != null) { + paint.setColor(Color.argb(30, 255, 0, 0)); + drawSelected(canvas, paint); + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + if (selectedView != null) { + drawSelected(canvas, selectionPaint); + } + } + + public void drawSelected(Canvas canvas, Paint paint) { + int[] absolutePosition = new int[2]; + int[] selectedViewPosition = new int[2]; + + selectedView.getLocationOnScreen(selectedViewPosition); + getLocationOnScreen(absolutePosition); + + int width = selectedView.getLayoutParams().width; + int height = selectedView.getLayoutParams().height; + + int x = selectedViewPosition[0] - absolutePosition[0]; + int y = selectedViewPosition[1] - absolutePosition[1]; + + canvas.drawRect(x, y, x + width, y + height, paint); + } + + + private void drawBackground(Canvas canvas) { + paint.setStyle(Paint.Style.FILL); + + int shapeSize = Math.round(getResources().getDimension(R.dimen.SizePt) * 7.2f); + boolean dark = true; + boolean start = true; + + for (int x = 0; x < width + shapeSize; x += shapeSize) { + for (int y = 0; y < height + shapeSize; y += shapeSize) { + paint.setColor(dark ? COLOR_DARK : COLOR_LIGHT); + canvas.drawRect(x, y, x + shapeSize, y + shapeSize, paint); + dark = !dark; + } + start = !start; + dark = start; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int measuredWidth = getMeasuredWidth(); + int measuredHeight = getMeasuredHeight(); + + if (measuredWidth != width || measuredHeight != height) { + width = measuredWidth; + height = measuredHeight; + refreshLayout(); + } + } + + public void refreshLayout() { + if (profile != null) { + profile.applyToView(ControllerItem.L, findViewById(R.id.button_l), width, height); + profile.applyToView(ControllerItem.R, findViewById(R.id.button_r), width, height); + profile.applyToView(ControllerItem.START, findViewById(R.id.button_start), width, height); + profile.applyToView(ControllerItem.SELECT, findViewById(R.id.button_select), width, height); + profile.applyToView(ControllerItem.DPAD, findViewById(R.id.dpad), width, height); + profile.applyToView(ControllerItem.GAMEPAD, findViewById(R.id.gamepad), width, height); + profile.applyToView(ControllerItem.JOYSTICK, findViewById(R.id.left_analog), width, height); + } + } + + public int getCurrentWidth() { + return width; + } + + public int getCurrentHeight() { + return height; + } + + public class MoveElementListener implements OnTouchListener { + private final ControllerItem id; + private final View view; + private final Vector2 downPosition = new Vector2(0.0f, 0.0f); + private boolean down = false; + + public MoveElementListener(ControllerItem id, View view) { + this.view = view; + this.id = id; + this.view.setOnTouchListener(this); + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouch(View v, MotionEvent event) { + if (!down) { + down = true; + downPosition.set(event.getX() - (view.getLayoutParams().width / 2.0f), event.getY() - (view.getLayoutParams().height / 2.0f)); + } + + int[] viewPosition = new int[2]; + getLocationOnScreen(viewPosition); + + int x = Math.max(0, Math.min(Math.round(event.getRawX() - viewPosition[0] - downPosition.x), width)); + int y = Math.max(0, Math.min(Math.round(event.getRawY() - viewPosition[1] - downPosition.y), height)); + + profile.setLocation(id, x, y, width, height); + profile.applyToView(id, view, width, height); + + if (changeListener != null) { + changeListener.run(id); + } + + selectedView = view; + + if (event.getAction() == MotionEvent.ACTION_UP) { + selectedView = null; + down = false; + invalidate(); + return false; + } else { + return true; + } + } + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerProfileManager.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerProfileManager.java new file mode 100644 index 000000000..c5b753d25 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/ControllerProfileManager.java @@ -0,0 +1,103 @@ +package com.panda3ds.pandroid.view.controller.mapping; + +import android.annotation.SuppressLint; +import android.view.Gravity; + +import com.panda3ds.pandroid.data.GsonConfigParser; +import com.panda3ds.pandroid.utils.Constants; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +public class ControllerProfileManager { + + public static final GsonConfigParser parser; + private static final DataModel data; + + static { + parser = new GsonConfigParser(Constants.PREF_SCREEN_CONTROLLER_PROFILES); + data = parser.load(DataModel.class); + if (data.profiles.size() == 0) { + add(makeDefaultProfile()); + } + } + + public static void remove(String id) { + data.profiles.remove(id); + save(); + } + + public static void add(Profile profile) { + data.profiles.put(profile.getId(), profile); + save(); + } + + public static List listAll() { + return new ArrayList<>(data.profiles.values()); + } + + public static int getProfileCount() { + return data.profiles.size(); + } + + public static Profile getDefaultProfile() { + if (data.profiles.containsKey(data.profileId)) { + return data.profiles.get(data.profileId); + } else if (getProfileCount() > 0) { + data.profileId = data.profiles.keySet().iterator().next(); + save(); + return getDefaultProfile(); + } else { + add(makeDefaultProfile()); + return getDefaultProfile(); + } + } + + private static void save() { + if ((!data.profiles.containsKey(data.profileId)) && getProfileCount() > 0) { + data.profileId = data.profiles.keySet().iterator().next(); + } + parser.save(data); + } + + public static Profile makeDefaultProfile() { + return new Profile(UUID.randomUUID().toString(), "Default", createDefaultLayout(), createDefaultLayout()); + } + + @SuppressLint("RtlHardcoded") + public static Layout createDefaultLayout() { + Layout layout = new Layout(); + + layout.setLocation(ControllerItem.L, new Location(39, 145, Gravity.LEFT, true)); + layout.setLocation(ControllerItem.R, new Location(39, 145, Gravity.RIGHT, true)); + + layout.setLocation(ControllerItem.SELECT, new Location(32, 131, Gravity.LEFT, true)); + layout.setLocation(ControllerItem.START, new Location(32, 131, Gravity.RIGHT, true)); + + layout.setLocation(ControllerItem.DPAD, new Location(42, 90, Gravity.LEFT, true)); + layout.setLocation(ControllerItem.JOYSTICK, new Location(74, 45, Gravity.LEFT, true)); + layout.setLocation(ControllerItem.GAMEPAD, new Location(42, 75, Gravity.RIGHT, true)); + + return layout; + } + + public static Profile get(String profile) { + return data.profiles.getOrDefault(profile, null); + } + + public static void setDefaultProfileId(String id) { + if (data.profiles.containsKey(id) && !Objects.equals(id, data.profileId)) { + data.profileId = id; + save(); + } + } + + public static class DataModel { + public final Map profiles = new HashMap<>(); + public String profileId; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Layout.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Layout.java new file mode 100644 index 000000000..a2e5f3ceb --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Layout.java @@ -0,0 +1,36 @@ +package com.panda3ds.pandroid.view.controller.mapping; + + +import androidx.annotation.NonNull; + +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public class Layout { + private final Map mapLocations = new HashMap<>(); + + public void setLocation(ControllerItem item, Location location) { + mapLocations.put(item, location); + } + + @NotNull + public Location getLocation(ControllerItem item) { + if (!mapLocations.containsKey(item)) { + setLocation(item, new Location()); + } + return Objects.requireNonNull(mapLocations.get(item)); + } + + @NonNull + @Override + public Layout clone() { + Layout cloned = new Layout(); + for (ControllerItem key : mapLocations.keySet()) { + cloned.setLocation(key, getLocation(key).clone()); + } + return cloned; + } +} diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Location.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Location.java new file mode 100644 index 000000000..1d28758a1 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Location.java @@ -0,0 +1,57 @@ +package com.panda3ds.pandroid.view.controller.mapping; + +import android.view.Gravity; + +import androidx.annotation.NonNull; + +public class Location { + private float x = 0.0f; + private float y = 0.0f; + private int gravity = Gravity.LEFT; + private boolean visible = false; + + public Location() {} + + public Location(float x, float y, int gravity, boolean visible) { + this.x = x; + this.y = y; + this.gravity = gravity; + this.visible = visible; + } + + public int getGravity() { + return gravity; + } + + public float getX() { + return x; + } + + public float getY() { + return y; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } + + public boolean isVisible() { + return visible; + } + + public void setGravity(int gravity) { + this.gravity = gravity; + } + + public void setPosition(float x, float y) { + this.x = x; + this.y = y; + } + + @NonNull + @Override + public Location clone() { + return new Location(x, y, gravity, visible); + } + +} \ No newline at end of file diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Profile.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Profile.java new file mode 100644 index 000000000..b537133b2 --- /dev/null +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/view/controller/mapping/Profile.java @@ -0,0 +1,104 @@ +package com.panda3ds.pandroid.view.controller.mapping; + +import android.view.Gravity; +import android.view.View; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; + +import com.panda3ds.pandroid.R; +import com.panda3ds.pandroid.app.PandroidApplication; + +import java.util.UUID; + +public class Profile { + private final String id; + private final Layout landscapeLayout; + private final Layout portraitLayout; + private String name; + + public Profile(String id, String name, Layout landscape, Layout portrait) { + this.id = id; + this.name = name; + this.landscapeLayout = landscape; + this.portraitLayout = portrait; + } + + public void applyToView(ControllerItem id, View view, int viewportWidth, int viewportHeight) { + float pt = view.getResources().getDimension(R.dimen.SizePt); + + int width = view.getLayoutParams().width; + int height = view.getLayoutParams().height; + + Layout layout = getLayoutBySize(viewportWidth, viewportHeight); + + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(width, height); + Location location = layout.getLocation(id); + + int x = Math.round(location.getX() * pt); + int y = Math.round(location.getY() * pt); + + params.gravity = location.getGravity() | Gravity.BOTTOM; + params.bottomMargin = Math.max(Math.min(y - (height / 2), viewportHeight - height), 0); + + int gravity = location.getGravity() & Gravity.HORIZONTAL_GRAVITY_MASK; + if (gravity == Gravity.RIGHT) { + params.rightMargin = Math.max(x - (width / 2), 0); + } else { + params.leftMargin = Math.max(x - (width / 2), 0); + } + + view.setVisibility(location.isVisible() ? View.VISIBLE : View.GONE); + view.setLayoutParams(params); + } + + public void setLocation(ControllerItem item, int x, int y, int viewportWidth, int viewportHeight) { + float pt = PandroidApplication.getAppContext().getResources().getDimension(R.dimen.SizePt); + + Layout layout = getLayoutBySize(viewportWidth, viewportHeight); + Location location = layout.getLocation(item); + + y = viewportHeight - y; + + if (x < viewportWidth / 2) { + location.setGravity(Gravity.LEFT); + location.setPosition(x / pt, y / pt); + } else { + x = (viewportWidth / 2) - (x - (viewportWidth / 2)); + location.setGravity(Gravity.RIGHT); + location.setPosition(x / pt, y / pt); + } + } + + + public void setName(String name) { + this.name = name; + } + + public void setVisible(ControllerItem id, boolean visible) { + landscapeLayout.getLocation(id).setVisible(visible); + portraitLayout.getLocation(id).setVisible(visible); + } + + private Layout getLayoutBySize(int width, int height) { + return width > height ? landscapeLayout : portraitLayout; + } + + public String getName() { + return name; + } + + public String getId() { + return id; + } + + @NonNull + @Override + public Profile clone() { + return new Profile(id, name, landscapeLayout.clone(), portraitLayout.clone()); + } + + public boolean isVisible(ControllerItem id) { + return landscapeLayout.getLocation(id).isVisible(); + } +} diff --git a/src/pandroid/app/src/main/res/color/red_color.xml b/src/pandroid/app/src/main/res/color/red_color.xml new file mode 100644 index 000000000..c42336f53 --- /dev/null +++ b/src/pandroid/app/src/main/res/color/red_color.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/drawable/alert_dialog_background.xml b/src/pandroid/app/src/main/res/drawable/alert_dialog_background.xml new file mode 100644 index 000000000..20a71e468 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/alert_dialog_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/drawable/ic_add.xml b/src/pandroid/app/src/main/res/drawable/ic_add.xml index 89633bb12..db709d0b3 100644 --- a/src/pandroid/app/src/main/res/drawable/ic_add.xml +++ b/src/pandroid/app/src/main/res/drawable/ic_add.xml @@ -1,4 +1,4 @@ - diff --git a/src/pandroid/app/src/main/res/drawable/ic_delete.xml b/src/pandroid/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 000000000..1efcc802a --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_rotate_screen.xml b/src/pandroid/app/src/main/res/drawable/ic_rotate_screen.xml new file mode 100644 index 000000000..4e95fb729 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_rotate_screen.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_save.xml b/src/pandroid/app/src/main/res/drawable/ic_save.xml new file mode 100644 index 000000000..82070aa2c --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_save.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/drawable/ic_visibility.xml b/src/pandroid/app/src/main/res/drawable/ic_visibility.xml new file mode 100644 index 000000000..b923c39d0 --- /dev/null +++ b/src/pandroid/app/src/main/res/drawable/ic_visibility.xml @@ -0,0 +1,5 @@ + + + diff --git a/src/pandroid/app/src/main/res/layout/controller_dpad.xml b/src/pandroid/app/src/main/res/layout/controller_dpad.xml index e3320dafb..b6f0f6263 100644 --- a/src/pandroid/app/src/main/res/layout/controller_dpad.xml +++ b/src/pandroid/app/src/main/res/layout/controller_dpad.xml @@ -1,5 +1,6 @@ - + android:alpha="0.6"> diff --git a/src/pandroid/app/src/main/res/layout/preference_controller_mapper.xml b/src/pandroid/app/src/main/res/layout/preference_controller_mapper.xml new file mode 100644 index 000000000..daecd2b23 --- /dev/null +++ b/src/pandroid/app/src/main/res/layout/preference_controller_mapper.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/values-pt-rBR/strings.xml b/src/pandroid/app/src/main/res/values-pt-rBR/strings.xml index d3966e18e..18edc50f0 100644 --- a/src/pandroid/app/src/main/res/values-pt-rBR/strings.xml +++ b/src/pandroid/app/src/main/res/values-pt-rBR/strings.xml @@ -27,4 +27,12 @@ Ações Sair Continuar + Salvo + Criar perfil + Entrada + Altere o mapeamento de controles, disposição de controle na tela etc. + Nome + Disposições de controle + Disposição de controle padrão + Nome Invalido \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/values/dimens.xml b/src/pandroid/app/src/main/res/values/dimens.xml new file mode 100644 index 000000000..5119ce72e --- /dev/null +++ b/src/pandroid/app/src/main/res/values/dimens.xml @@ -0,0 +1,4 @@ + + + 1pt + \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/values/strings.xml b/src/pandroid/app/src/main/res/values/strings.xml index 13db91522..2f08ab713 100644 --- a/src/pandroid/app/src/main/res/values/strings.xml +++ b/src/pandroid/app/src/main/res/values/strings.xml @@ -17,7 +17,7 @@ Dead zone Options Map physics controller or keyboard - Controller Mapping + Controller mapping Theme Set application theme Appearance @@ -28,4 +28,12 @@ Actions Exit Resume + Saved + Create profile + Input + Change input map, screen gamepad, etc. + Name + Screen gamepad layouts + Default screen gamepad layout + Invalid name \ No newline at end of file diff --git a/src/pandroid/app/src/main/res/values/themes.xml b/src/pandroid/app/src/main/res/values/themes.xml index c7d790808..2ade71029 100644 --- a/src/pandroid/app/src/main/res/values/themes.xml +++ b/src/pandroid/app/src/main/res/values/themes.xml @@ -3,7 +3,25 @@ + + + + + + + +