diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/CustomFilePickerFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/CustomFilePickerFragment.java deleted file mode 100644 index 933b4ad2ff..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/CustomFilePickerFragment.java +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.fragments; - -import android.net.Uri; -import android.os.Bundle; -import android.view.View; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.core.content.FileProvider; - -import com.nononsenseapps.filepicker.FilePickerFragment; - -import org.dolphinemu.dolphinemu.R; - -import java.io.File; -import java.util.HashSet; - -public class CustomFilePickerFragment extends FilePickerFragment -{ - public static final String KEY_EXTENSIONS = "KEY_EXTENSIONS"; - - private HashSet mExtensions; - - public void setExtensions(HashSet extensions) - { - Bundle b = getArguments(); - if (b == null) - b = new Bundle(); - - b.putSerializable(KEY_EXTENSIONS, extensions); - setArguments(b); - } - - @NonNull - @Override - public Uri toUri(@NonNull final File file) - { - return FileProvider - .getUriForFile(getContext(), - getContext().getApplicationContext().getPackageName() + ".filesprovider", - file); - } - - @Override public void onActivityCreated(Bundle savedInstanceState) - { - super.onActivityCreated(savedInstanceState); - - mExtensions = (HashSet) getArguments().getSerializable(KEY_EXTENSIONS); - - if (mode == MODE_DIR) - { - TextView ok = getActivity().findViewById(R.id.nnf_button_ok); - ok.setText(R.string.select_dir); - - TextView cancel = getActivity().findViewById(R.id.nnf_button_cancel); - cancel.setVisibility(View.GONE); - } - } - - @Override - protected boolean isItemVisible(@NonNull final File file) - { - // Some users jump to the conclusion that Dolphin isn't able to detect their - // files if the files don't show up in the file picker when mode == MODE_DIR. - // To avoid this, show files even when the user needs to select a directory. - - return (showHiddenItems || !file.isHidden()) && - (file.isDirectory() || - mExtensions.contains(fileExtension(file.getName()).toLowerCase())); - } - - @Override - public boolean isCheckable(@NonNull final File file) - { - // We need to make a small correction to the isCheckable logic due to - // overriding isItemVisible to show files when mode == MODE_DIR. - // AbstractFilePickerFragment always treats files as checkable when - // allowExistingFile == true, but we don't want files to be checkable when mode == MODE_DIR. - - return super.isCheckable(file) && !(mode == MODE_DIR && file.isFile()); - } - - private static String fileExtension(@NonNull String filename) - { - int i = filename.lastIndexOf('.'); - return i < 0 ? "" : filename.substring(i + 1); - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/CustomFilePickerFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/CustomFilePickerFragment.kt new file mode 100644 index 0000000000..aa74fb6f8a --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/CustomFilePickerFragment.kt @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.fragments + +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.widget.TextView +import androidx.core.content.FileProvider +import com.nononsenseapps.filepicker.FilePickerFragment +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable +import java.io.File +import java.util.Locale + +class CustomFilePickerFragment : FilePickerFragment() { + private var extensions: HashSet? = null + + fun setExtensions(extensions: HashSet?) { + var b = arguments + if (b == null) + b = Bundle() + b.putSerializable(KEY_EXTENSIONS, extensions) + arguments = b + } + + override fun toUri(file: File): Uri { + return FileProvider.getUriForFile( + requireContext(), + "${requireContext().applicationContext.packageName}.filesprovider", + file + ) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + extensions = requireArguments().serializable(KEY_EXTENSIONS) as HashSet? + + if (mode == MODE_DIR) { + val ok = requireActivity().findViewById(R.id.nnf_button_ok) + ok.setText(R.string.select_dir) + + val cancel = requireActivity().findViewById(R.id.nnf_button_cancel) + cancel.visibility = View.GONE + } + } + + override fun isItemVisible(file: File): Boolean { + // Some users jump to the conclusion that Dolphin isn't able to detect their + // files if the files don't show up in the file picker when mode == MODE_DIR. + // To avoid this, show files even when the user needs to select a directory. + return (showHiddenItems || !file.isHidden) && + (file.isDirectory || extensions!!.contains(fileExtension(file.name).lowercase(Locale.getDefault()))) + } + + override fun isCheckable(file: File): Boolean { + // We need to make a small correction to the isCheckable logic due to + // overriding isItemVisible to show files when mode == MODE_DIR. + // AbstractFilePickerFragment always treats files as checkable when + // allowExistingFile == true, but we don't want files to be checkable when mode == MODE_DIR. + return super.isCheckable(file) && !(mode == MODE_DIR && file.isFile) + } + + companion object { + const val KEY_EXTENSIONS = "KEY_EXTENSIONS" + + private fun fileExtension(filename: String): String { + val i = filename.lastIndexOf('.') + return if (i < 0) "" else filename.substring(i + 1) + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java deleted file mode 100644 index 3a7c9c1067..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.java +++ /dev/null @@ -1,355 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.fragments; - -import android.content.Context; -import android.graphics.Rect; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.SurfaceHolder; -import android.view.SurfaceView; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; - -import org.dolphinemu.dolphinemu.NativeLibrary; -import org.dolphinemu.dolphinemu.activities.EmulationActivity; -import org.dolphinemu.dolphinemu.databinding.FragmentEmulationBinding; -import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; -import org.dolphinemu.dolphinemu.features.settings.model.Settings; -import org.dolphinemu.dolphinemu.overlay.InputOverlay; -import org.dolphinemu.dolphinemu.utils.Log; - -import java.io.File; - -public final class EmulationFragment extends Fragment implements SurfaceHolder.Callback -{ - private static final String KEY_GAMEPATHS = "gamepaths"; - private static final String KEY_RIIVOLUTION = "riivolution"; - private static final String KEY_SYSTEM_MENU = "systemMenu"; - - private InputOverlay mInputOverlay; - - private String[] mGamePaths; - private boolean mRiivolution; - private boolean mRunWhenSurfaceIsValid; - private boolean mLoadPreviousTemporaryState; - private boolean mLaunchSystemMenu; - - private EmulationActivity activity; - - private FragmentEmulationBinding mBinding; - - public static EmulationFragment newInstance(String[] gamePaths, boolean riivolution, - boolean systemMenu) - { - Bundle args = new Bundle(); - args.putStringArray(KEY_GAMEPATHS, gamePaths); - args.putBoolean(KEY_RIIVOLUTION, riivolution); - args.putBoolean(KEY_SYSTEM_MENU, systemMenu); - - EmulationFragment fragment = new EmulationFragment(); - fragment.setArguments(args); - return fragment; - } - - @Override - public void onAttach(@NonNull Context context) - { - super.onAttach(context); - - if (context instanceof EmulationActivity) - { - activity = (EmulationActivity) context; - NativeLibrary.setEmulationActivity((EmulationActivity) context); - } - else - { - throw new IllegalStateException("EmulationFragment must have EmulationActivity parent"); - } - } - - /** - * Initialize anything that doesn't depend on the layout / views in here. - */ - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - mGamePaths = getArguments().getStringArray(KEY_GAMEPATHS); - mRiivolution = getArguments().getBoolean(KEY_RIIVOLUTION); - mLaunchSystemMenu = getArguments().getBoolean(KEY_SYSTEM_MENU); - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) - { - mBinding = FragmentEmulationBinding.inflate(inflater, container, false); - return mBinding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) - { - // The new Surface created here will get passed to the native code via onSurfaceChanged. - SurfaceView surfaceView = mBinding.surfaceEmulation; - surfaceView.getHolder().addCallback(this); - - mInputOverlay = mBinding.surfaceInputOverlay; - - Button doneButton = mBinding.doneControlConfig; - if (doneButton != null) - { - doneButton.setOnClickListener(v -> stopConfiguringControls()); - } - - if (mInputOverlay != null) - { - view.post(() -> - { - int overlayX = mInputOverlay.getLeft(); - int overlayY = mInputOverlay.getTop(); - mInputOverlay.setSurfacePosition(new Rect( - surfaceView.getLeft() - overlayX, surfaceView.getTop() - overlayY, - surfaceView.getRight() - overlayX, surfaceView.getBottom() - overlayY)); - }); - } - } - - @Override - public void onDestroyView() - { - super.onDestroyView(); - mBinding = null; - } - - @Override - public void onResume() - { - super.onResume(); - - if (mInputOverlay != null && NativeLibrary.IsGameMetadataValid()) - mInputOverlay.refreshControls(); - - run(activity.isActivityRecreated()); - } - - @Override - public void onPause() - { - if (NativeLibrary.IsRunningAndUnpaused() && !NativeLibrary.IsShowingAlertMessage()) - { - Log.debug("[EmulationFragment] Pausing emulation."); - NativeLibrary.PauseEmulation(); - } - - super.onPause(); - } - - @Override - public void onDestroy() - { - if (mInputOverlay != null) - mInputOverlay.onDestroy(); - - super.onDestroy(); - } - - @Override - public void onDetach() - { - NativeLibrary.clearEmulationActivity(); - super.onDetach(); - } - - public void toggleInputOverlayVisibility(Settings settings) - { - BooleanSetting.MAIN_SHOW_INPUT_OVERLAY - .setBoolean(settings, !BooleanSetting.MAIN_SHOW_INPUT_OVERLAY.getBoolean()); - - if (mInputOverlay != null) - mInputOverlay.refreshControls(); - } - - public void initInputPointer() - { - if (mInputOverlay != null) - mInputOverlay.initTouchPointer(); - } - - public void refreshInputOverlay() - { - if (mInputOverlay != null) - mInputOverlay.refreshControls(); - } - - public void refreshOverlayPointer() - { - if (mInputOverlay != null) - mInputOverlay.refreshOverlayPointer(); - } - - public void resetInputOverlay() - { - if (mInputOverlay != null) - mInputOverlay.resetButtonPlacement(); - } - - @Override - public void surfaceCreated(@NonNull SurfaceHolder holder) - { - // We purposely don't do anything here. - // All work is done in surfaceChanged, which we are guaranteed to get even for surface creation. - } - - @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) - { - Log.debug("[EmulationFragment] Surface changed. Resolution: " + width + "x" + height); - NativeLibrary.SurfaceChanged(holder.getSurface()); - if (mRunWhenSurfaceIsValid) - { - runWithValidSurface(); - } - } - - @Override - public void surfaceDestroyed(@NonNull SurfaceHolder holder) - { - Log.debug("[EmulationFragment] Surface destroyed."); - NativeLibrary.SurfaceDestroyed(); - mRunWhenSurfaceIsValid = true; - } - - public void stopEmulation() - { - Log.debug("[EmulationFragment] Stopping emulation."); - NativeLibrary.StopEmulation(); - } - - public void startConfiguringControls() - { - if (mInputOverlay != null) - { - mBinding.doneControlConfig.setVisibility(View.VISIBLE); - mInputOverlay.setEditMode(true); - } - } - - public void stopConfiguringControls() - { - if (mInputOverlay != null) - { - mBinding.doneControlConfig.setVisibility(View.GONE); - mInputOverlay.setEditMode(false); - } - } - - public boolean isConfiguringControls() - { - return mInputOverlay != null && mInputOverlay.isInEditMode(); - } - - private void run(boolean isActivityRecreated) - { - if (isActivityRecreated) - { - if (NativeLibrary.IsRunning()) - { - mLoadPreviousTemporaryState = false; - deleteFile(getTemporaryStateFilePath()); - } - else - { - mLoadPreviousTemporaryState = true; - } - } - else - { - Log.debug("[EmulationFragment] activity resumed or fresh start"); - mLoadPreviousTemporaryState = false; - // activity resumed without being killed or this is the first run - deleteFile(getTemporaryStateFilePath()); - } - - // If the surface is set, run now. Otherwise, wait for it to get set. - if (NativeLibrary.HasSurface()) - { - runWithValidSurface(); - } - else - { - mRunWhenSurfaceIsValid = true; - } - } - - private void runWithValidSurface() - { - mRunWhenSurfaceIsValid = false; - if (!NativeLibrary.IsRunning()) - { - NativeLibrary.SetIsBooting(); - - Thread emulationThread = new Thread(() -> - { - if (mLoadPreviousTemporaryState) - { - Log.debug("[EmulationFragment] Starting emulation thread from previous state."); - NativeLibrary.Run(mGamePaths, mRiivolution, getTemporaryStateFilePath(), true); - } - if (mLaunchSystemMenu) - { - Log.debug("[EmulationFragment] Starting emulation thread for the Wii Menu."); - NativeLibrary.RunSystemMenu(); - } - else - { - Log.debug("[EmulationFragment] Starting emulation thread."); - NativeLibrary.Run(mGamePaths, mRiivolution); - } - EmulationActivity.stopIgnoringLaunchRequests(); - }, "NativeEmulation"); - emulationThread.start(); - } - else - { - if (!EmulationActivity.Companion.getHasUserPausedEmulation() && - !NativeLibrary.IsShowingAlertMessage()) - { - Log.debug("[EmulationFragment] Resuming emulation."); - NativeLibrary.UnPauseEmulation(); - } - } - } - - public void saveTemporaryState() - { - NativeLibrary.SaveStateAs(getTemporaryStateFilePath(), true); - } - - private String getTemporaryStateFilePath() - { - return getContext().getFilesDir() + File.separator + "temp.sav"; - } - - private static void deleteFile(String path) - { - try - { - File file = new File(path); - if (!file.delete()) - { - Log.error("[EmulationFragment] Failed to delete " + file.getAbsolutePath()); - } - } - catch (Exception ignored) - { - } - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt new file mode 100644 index 0000000000..e403e7ef5d --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.fragments + +import android.content.Context +import android.graphics.Rect +import android.os.Bundle +import android.view.LayoutInflater +import android.view.SurfaceHolder +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import org.dolphinemu.dolphinemu.NativeLibrary +import org.dolphinemu.dolphinemu.activities.EmulationActivity +import org.dolphinemu.dolphinemu.databinding.FragmentEmulationBinding +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting +import org.dolphinemu.dolphinemu.features.settings.model.Settings +import org.dolphinemu.dolphinemu.overlay.InputOverlay +import org.dolphinemu.dolphinemu.utils.Log +import java.io.File + +class EmulationFragment : Fragment(), SurfaceHolder.Callback { + private var inputOverlay: InputOverlay? = null + + private var gamePaths: Array? = null + private var riivolution = false + private var runWhenSurfaceIsValid = false + private var loadPreviousTemporaryState = false + private var launchSystemMenu = false + + private var emulationActivity: EmulationActivity? = null + + private var _binding: FragmentEmulationBinding? = null + private val binding get() = _binding!! + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context is EmulationActivity) { + emulationActivity = context + NativeLibrary.setEmulationActivity(context) + } else { + throw IllegalStateException("EmulationFragment must have EmulationActivity parent") + } + } + + /** + * Initialize anything that doesn't depend on the layout / views in here. + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + requireArguments().apply { + gamePaths = getStringArray(KEY_GAMEPATHS) + riivolution = getBoolean(KEY_RIIVOLUTION) + launchSystemMenu = getBoolean(KEY_SYSTEM_MENU) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentEmulationBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + // The new Surface created here will get passed to the native code via onSurfaceChanged. + val surfaceView = binding.surfaceEmulation + surfaceView.holder.addCallback(this) + + inputOverlay = binding.surfaceInputOverlay + + val doneButton = binding.doneControlConfig + doneButton?.setOnClickListener { stopConfiguringControls() } + + if (inputOverlay != null) { + view.post { + val overlayX = inputOverlay!!.left + val overlayY = inputOverlay!!.top + inputOverlay?.setSurfacePosition( + Rect( + surfaceView.left - overlayX, + surfaceView.top - overlayY, + surfaceView.right - overlayX, + surfaceView.bottom - overlayY + ) + ) + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + override fun onResume() { + super.onResume() + if (NativeLibrary.IsGameMetadataValid()) + inputOverlay?.refreshControls() + + run(emulationActivity!!.isActivityRecreated) + } + + override fun onPause() { + if (NativeLibrary.IsRunningAndUnpaused() && !NativeLibrary.IsShowingAlertMessage()) { + Log.debug("[EmulationFragment] Pausing emulation.") + NativeLibrary.PauseEmulation() + } + super.onPause() + } + + override fun onDestroy() { + inputOverlay?.onDestroy() + super.onDestroy() + } + + override fun onDetach() { + NativeLibrary.clearEmulationActivity() + super.onDetach() + } + + fun toggleInputOverlayVisibility(settings: Settings?) { + BooleanSetting.MAIN_SHOW_INPUT_OVERLAY.setBoolean( + settings!!, + !BooleanSetting.MAIN_SHOW_INPUT_OVERLAY.boolean + ) + + inputOverlay?.refreshControls() + } + + fun initInputPointer() = inputOverlay?.initTouchPointer() + + fun refreshInputOverlay() = inputOverlay?.refreshControls() + + fun refreshOverlayPointer() = inputOverlay?.refreshOverlayPointer() + + fun resetInputOverlay() = inputOverlay?.resetButtonPlacement() + + override fun surfaceCreated(holder: SurfaceHolder) { + // We purposely don't do anything here. + // All work is done in surfaceChanged, which we are guaranteed to get even for surface creation. + } + + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + Log.debug("[EmulationFragment] Surface changed. Resolution: $width x $height") + NativeLibrary.SurfaceChanged(holder.surface) + if (runWhenSurfaceIsValid) { + runWithValidSurface() + } + } + + override fun surfaceDestroyed(holder: SurfaceHolder) { + Log.debug("[EmulationFragment] Surface destroyed.") + NativeLibrary.SurfaceDestroyed() + runWhenSurfaceIsValid = true + } + + fun stopEmulation() { + Log.debug("[EmulationFragment] Stopping emulation.") + NativeLibrary.StopEmulation() + } + + fun startConfiguringControls() { + binding.doneControlConfig?.visibility = View.VISIBLE + inputOverlay?.editMode = true + } + + fun stopConfiguringControls() { + binding.doneControlConfig?.visibility = View.GONE + inputOverlay?.editMode = false + } + + val isConfiguringControls: Boolean + get() = inputOverlay != null && inputOverlay!!.isInEditMode + + private fun run(isActivityRecreated: Boolean) { + if (isActivityRecreated) { + if (NativeLibrary.IsRunning()) { + loadPreviousTemporaryState = false + deleteFile(temporaryStateFilePath) + } else { + loadPreviousTemporaryState = true + } + } else { + Log.debug("[EmulationFragment] activity resumed or fresh start") + loadPreviousTemporaryState = false + // activity resumed without being killed or this is the first run + deleteFile(temporaryStateFilePath) + } + + // If the surface is set, run now. Otherwise, wait for it to get set. + if (NativeLibrary.HasSurface()) { + runWithValidSurface() + } else { + runWhenSurfaceIsValid = true + } + } + + private fun runWithValidSurface() { + runWhenSurfaceIsValid = false + if (!NativeLibrary.IsRunning()) { + NativeLibrary.SetIsBooting() + val emulationThread = Thread({ + if (loadPreviousTemporaryState) { + Log.debug("[EmulationFragment] Starting emulation thread from previous state.") + NativeLibrary.Run(gamePaths, riivolution, temporaryStateFilePath, true) + } + if (launchSystemMenu) { + Log.debug("[EmulationFragment] Starting emulation thread for the Wii Menu.") + NativeLibrary.RunSystemMenu() + } else { + Log.debug("[EmulationFragment] Starting emulation thread.") + NativeLibrary.Run(gamePaths, riivolution) + } + EmulationActivity.stopIgnoringLaunchRequests() + }, "NativeEmulation") + emulationThread.start() + } else { + if (!EmulationActivity.hasUserPausedEmulation && !NativeLibrary.IsShowingAlertMessage()) { + Log.debug("[EmulationFragment] Resuming emulation.") + NativeLibrary.UnPauseEmulation() + } + } + } + + fun saveTemporaryState() = NativeLibrary.SaveStateAs(temporaryStateFilePath, true) + + private val temporaryStateFilePath: String + get() = "${requireContext().filesDir}${File.separator}temp.sav" + + companion object { + private const val KEY_GAMEPATHS = "gamepaths" + private const val KEY_RIIVOLUTION = "riivolution" + private const val KEY_SYSTEM_MENU = "systemMenu" + + fun newInstance( + gamePaths: Array?, + riivolution: Boolean, + systemMenu: Boolean + ): EmulationFragment { + val args = Bundle() + args.apply { + putStringArray(KEY_GAMEPATHS, gamePaths) + putBoolean(KEY_RIIVOLUTION, riivolution) + putBoolean(KEY_SYSTEM_MENU, systemMenu) + } + val fragment = EmulationFragment() + fragment.arguments = args + return fragment + } + + private fun deleteFile(path: String) { + try { + val file = File(path) + if (!file.delete()) { + Log.error("[EmulationFragment] Failed to delete ${file.absolutePath}") + } + } catch (ignored: Exception) { + } + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.java deleted file mode 100644 index 5f8ce06013..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.java +++ /dev/null @@ -1,228 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.fragments; - -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.util.SparseIntArray; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.LinearLayout; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.graphics.Insets; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowInsetsCompat; -import androidx.fragment.app.Fragment; - -import com.google.android.material.color.MaterialColors; -import com.google.android.material.elevation.ElevationOverlayProvider; - -import org.dolphinemu.dolphinemu.NativeLibrary; -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.activities.EmulationActivity; -import org.dolphinemu.dolphinemu.databinding.FragmentIngameMenuBinding; -import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; -import org.dolphinemu.dolphinemu.features.settings.model.IntSetting; -import org.dolphinemu.dolphinemu.utils.InsetsHelper; -import org.dolphinemu.dolphinemu.utils.ThemeHelper; - -public final class MenuFragment extends Fragment implements View.OnClickListener -{ - private static final String KEY_TITLE = "title"; - private static final String KEY_WII = "wii"; - private static SparseIntArray buttonsActionsMap = new SparseIntArray(); - - private int mCutInset = 0; - - static - { - buttonsActionsMap - .append(R.id.menu_pause_emulation, EmulationActivity.MENU_ACTION_PAUSE_EMULATION); - buttonsActionsMap - .append(R.id.menu_unpause_emulation, EmulationActivity.MENU_ACTION_UNPAUSE_EMULATION); - buttonsActionsMap - .append(R.id.menu_take_screenshot, EmulationActivity.MENU_ACTION_TAKE_SCREENSHOT); - buttonsActionsMap.append(R.id.menu_quicksave, EmulationActivity.MENU_ACTION_QUICK_SAVE); - buttonsActionsMap.append(R.id.menu_quickload, EmulationActivity.MENU_ACTION_QUICK_LOAD); - buttonsActionsMap - .append(R.id.menu_emulation_save_root, EmulationActivity.MENU_ACTION_SAVE_ROOT); - buttonsActionsMap - .append(R.id.menu_emulation_load_root, EmulationActivity.MENU_ACTION_LOAD_ROOT); - buttonsActionsMap - .append(R.id.menu_overlay_controls, EmulationActivity.MENU_ACTION_OVERLAY_CONTROLS); - buttonsActionsMap - .append(R.id.menu_refresh_wiimotes, EmulationActivity.MENU_ACTION_REFRESH_WIIMOTES); - buttonsActionsMap.append(R.id.menu_change_disc, EmulationActivity.MENU_ACTION_CHANGE_DISC); - buttonsActionsMap.append(R.id.menu_exit, EmulationActivity.MENU_ACTION_EXIT); - buttonsActionsMap.append(R.id.menu_settings, EmulationActivity.MENU_ACTION_SETTINGS); - buttonsActionsMap.append(R.id.menu_skylanders, EmulationActivity.MENU_ACTION_SKYLANDERS); - buttonsActionsMap.append(R.id.menu_infinitybase, EmulationActivity.MENU_ACTION_INFINITY_BASE); - } - - private FragmentIngameMenuBinding mBinding; - - public static MenuFragment newInstance() - { - MenuFragment fragment = new MenuFragment(); - - Bundle arguments = new Bundle(); - if (NativeLibrary.IsGameMetadataValid()) - { - arguments.putString(KEY_TITLE, NativeLibrary.GetCurrentTitleDescription()); - arguments.putBoolean(KEY_WII, NativeLibrary.IsEmulatingWii()); - } - fragment.setArguments(arguments); - - return fragment; - } - - @NonNull - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) - { - mBinding = FragmentIngameMenuBinding.inflate(inflater, container, false); - return mBinding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) - { - if (IntSetting.MAIN_INTERFACE_THEME.getInt() != ThemeHelper.DEFAULT) - { - @ColorInt int color = new ElevationOverlayProvider(view.getContext()).compositeOverlay( - MaterialColors.getColor(view, R.attr.colorSurface), - view.getElevation()); - view.setBackgroundColor(color); - } - - setInsets(); - updatePauseUnpauseVisibility(); - - if (!requireActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) - { - mBinding.menuOverlayControls.setVisibility(View.GONE); - } - - if (!getArguments().getBoolean(KEY_WII, true)) - { - mBinding.menuRefreshWiimotes.setVisibility(View.GONE); - mBinding.menuSkylanders.setVisibility(View.GONE); - } - - if (!BooleanSetting.MAIN_EMULATE_SKYLANDER_PORTAL.getBoolean()) - { - mBinding.menuSkylanders.setVisibility(View.GONE); - } - - LinearLayout options = mBinding.layoutOptions; - for (int childIndex = 0; childIndex < options.getChildCount(); childIndex++) - { - Button button = (Button) options.getChildAt(childIndex); - - button.setOnClickListener(this); - } - - mBinding.menuExit.setOnClickListener(this); - - String title = getArguments().getString(KEY_TITLE, null); - if (title != null) - { - mBinding.textGameTitle.setText(title); - } - } - - private void setInsets() - { - ViewCompat.setOnApplyWindowInsetsListener(mBinding.getRoot(), (v, windowInsets) -> - { - Insets cutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()); - mCutInset = cutInsets.left; - - int left = 0; - int right = 0; - if (ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_LTR) - { - left = cutInsets.left; - } - else - { - right = cutInsets.right; - } - - v.post(() -> NativeLibrary.SetObscuredPixelsLeft(v.getWidth())); - - // Don't use padding if the navigation bar isn't in the way - if (InsetsHelper.getBottomPaddingRequired(requireActivity()) > 0) - { - v.setPadding(left, cutInsets.top, right, - cutInsets.bottom + InsetsHelper.getNavigationBarHeight(requireContext())); - } - else - { - v.setPadding(left, cutInsets.top, right, - cutInsets.bottom + getResources().getDimensionPixelSize(R.dimen.spacing_large)); - } - return windowInsets; - }); - } - - @Override - public void onResume() - { - super.onResume(); - - boolean savestatesEnabled = BooleanSetting.MAIN_ENABLE_SAVESTATES.getBoolean(); - int savestateVisibility = savestatesEnabled ? View.VISIBLE : View.GONE; - mBinding.menuQuicksave.setVisibility(savestateVisibility); - mBinding.menuQuickload.setVisibility(savestateVisibility); - mBinding.menuEmulationSaveRoot.setVisibility(savestateVisibility); - mBinding.menuEmulationLoadRoot.setVisibility(savestateVisibility); - } - - @Override - public void onDestroyView() - { - super.onDestroyView(); - - NativeLibrary.SetObscuredPixelsLeft(mCutInset); - mBinding = null; - } - - private void updatePauseUnpauseVisibility() - { - boolean paused = EmulationActivity.Companion.getHasUserPausedEmulation(); - - mBinding.menuUnpauseEmulation.setVisibility(paused ? View.VISIBLE : View.GONE); - mBinding.menuPauseEmulation.setVisibility(paused ? View.GONE : View.VISIBLE); - } - - @Override - public void onClick(View button) - { - int action = buttonsActionsMap.get(button.getId()); - EmulationActivity activity = (EmulationActivity) requireActivity(); - - if (action == EmulationActivity.MENU_ACTION_OVERLAY_CONTROLS) - { - // We could use the button parameter as the anchor here, but this often results in a tiny menu - // (because the button often is in the middle of the screen), so let's use mTitleText instead - activity.showOverlayControlsMenu(mBinding.textGameTitle); - } - else if (action >= 0) - { - activity.handleMenuAction(action); - } - - if (action == EmulationActivity.MENU_ACTION_PAUSE_EMULATION || - action == EmulationActivity.MENU_ACTION_UNPAUSE_EMULATION) - { - updatePauseUnpauseVisibility(); - } - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.kt new file mode 100644 index 0000000000..a90d07930c --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/MenuFragment.kt @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.fragments + +import android.content.pm.PackageManager +import android.os.Bundle +import android.util.SparseIntArray +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import androidx.annotation.ColorInt +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.fragment.app.Fragment +import com.google.android.material.color.MaterialColors +import com.google.android.material.elevation.ElevationOverlayProvider +import org.dolphinemu.dolphinemu.NativeLibrary +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.activities.EmulationActivity +import org.dolphinemu.dolphinemu.databinding.FragmentIngameMenuBinding +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting +import org.dolphinemu.dolphinemu.features.settings.model.IntSetting +import org.dolphinemu.dolphinemu.utils.InsetsHelper +import org.dolphinemu.dolphinemu.utils.ThemeHelper + +class MenuFragment : Fragment(), View.OnClickListener { + private var cutInset = 0 + + private var _binding: FragmentIngameMenuBinding? = null + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentIngameMenuBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + if (IntSetting.MAIN_INTERFACE_THEME.int != ThemeHelper.DEFAULT) { + @ColorInt val color = ElevationOverlayProvider(view.context).compositeOverlay( + MaterialColors.getColor(view, R.attr.colorSurface), + view.elevation + ) + view.setBackgroundColor(color) + } + + setInsets() + updatePauseUnpauseVisibility() + + if (!requireActivity().packageManager.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) { + binding.menuOverlayControls.visibility = View.GONE + } + + if (!requireArguments().getBoolean(KEY_WII, true)) { + binding.menuRefreshWiimotes.visibility = View.GONE + binding.menuSkylanders.visibility = View.GONE + } + + if (!BooleanSetting.MAIN_EMULATE_SKYLANDER_PORTAL.boolean) { + binding.menuSkylanders.visibility = View.GONE + } + + val options = binding.layoutOptions + for (childIndex in 0 until options.childCount) { + val button = options.getChildAt(childIndex) as Button + button.setOnClickListener(this) + } + + binding.menuExit.setOnClickListener(this) + + val title = requireArguments().getString(KEY_TITLE, null) + if (title != null) { + binding.textGameTitle.text = title + } + } + + private fun setInsets() = + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v: View, windowInsets: WindowInsetsCompat -> + val cutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) + cutInset = cutInsets.left + var left = 0 + var right = 0 + if (ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_LTR) { + left = cutInsets.left + } else { + right = cutInsets.right + } + v.post { NativeLibrary.SetObscuredPixelsLeft(v.width) } + + // Don't use padding if the navigation bar isn't in the way + if (InsetsHelper.getBottomPaddingRequired(requireActivity()) > 0) { + v.setPadding( + left, cutInsets.top, right, + cutInsets.bottom + InsetsHelper.getNavigationBarHeight(requireContext()) + ) + } else { + v.setPadding( + left, cutInsets.top, right, + cutInsets.bottom + resources.getDimensionPixelSize(R.dimen.spacing_large) + ) + } + windowInsets + } + + override fun onResume() { + super.onResume() + val savestatesEnabled = BooleanSetting.MAIN_ENABLE_SAVESTATES.boolean + val savestateVisibility = if (savestatesEnabled) View.VISIBLE else View.GONE + binding.menuQuicksave.visibility = savestateVisibility + binding.menuQuickload.visibility = savestateVisibility + binding.menuEmulationSaveRoot.visibility = savestateVisibility + binding.menuEmulationLoadRoot.visibility = savestateVisibility + } + + override fun onDestroyView() { + super.onDestroyView() + NativeLibrary.SetObscuredPixelsLeft(cutInset) + _binding = null + } + + private fun updatePauseUnpauseVisibility() { + val paused = EmulationActivity.hasUserPausedEmulation + binding.menuUnpauseEmulation.visibility = if (paused) View.VISIBLE else View.GONE + binding.menuPauseEmulation.visibility = if (paused) View.GONE else View.VISIBLE + } + + override fun onClick(button: View) { + val action = buttonsActionsMap[button.id] + val activity = requireActivity() as EmulationActivity + + if (action == EmulationActivity.MENU_ACTION_OVERLAY_CONTROLS) { + // We could use the button parameter as the anchor here, but this often results in a tiny menu + // (because the button often is in the middle of the screen), so let's use mTitleText instead + activity.showOverlayControlsMenu(binding.textGameTitle) + } else if (action >= 0) { + activity.handleMenuAction(action) + } + + if (action == EmulationActivity.MENU_ACTION_PAUSE_EMULATION || + action == EmulationActivity.MENU_ACTION_UNPAUSE_EMULATION + ) { + updatePauseUnpauseVisibility() + } + } + + companion object { + private const val KEY_TITLE = "title" + private const val KEY_WII = "wii" + private val buttonsActionsMap = SparseIntArray() + + init { + buttonsActionsMap.append( + R.id.menu_pause_emulation, + EmulationActivity.MENU_ACTION_PAUSE_EMULATION + ) + buttonsActionsMap.append( + R.id.menu_unpause_emulation, + EmulationActivity.MENU_ACTION_UNPAUSE_EMULATION + ) + buttonsActionsMap.append( + R.id.menu_take_screenshot, + EmulationActivity.MENU_ACTION_TAKE_SCREENSHOT + ) + buttonsActionsMap.append(R.id.menu_quicksave, EmulationActivity.MENU_ACTION_QUICK_SAVE) + buttonsActionsMap.append(R.id.menu_quickload, EmulationActivity.MENU_ACTION_QUICK_LOAD) + buttonsActionsMap.append( + R.id.menu_emulation_save_root, + EmulationActivity.MENU_ACTION_SAVE_ROOT + ) + buttonsActionsMap.append( + R.id.menu_emulation_load_root, + EmulationActivity.MENU_ACTION_LOAD_ROOT + ) + buttonsActionsMap.append( + R.id.menu_overlay_controls, + EmulationActivity.MENU_ACTION_OVERLAY_CONTROLS + ) + buttonsActionsMap.append( + R.id.menu_refresh_wiimotes, + EmulationActivity.MENU_ACTION_REFRESH_WIIMOTES + ) + buttonsActionsMap.append( + R.id.menu_change_disc, + EmulationActivity.MENU_ACTION_CHANGE_DISC + ) + buttonsActionsMap.append(R.id.menu_exit, EmulationActivity.MENU_ACTION_EXIT) + buttonsActionsMap.append(R.id.menu_settings, EmulationActivity.MENU_ACTION_SETTINGS) + buttonsActionsMap.append(R.id.menu_skylanders, EmulationActivity.MENU_ACTION_SKYLANDERS) + buttonsActionsMap.append( + R.id.menu_infinitybase, + EmulationActivity.MENU_ACTION_INFINITY_BASE + ) + } + + fun newInstance(): MenuFragment { + val fragment = MenuFragment() + val arguments = Bundle() + if (NativeLibrary.IsGameMetadataValid()) { + arguments.putString(KEY_TITLE, NativeLibrary.GetCurrentTitleDescription()) + arguments.putBoolean(KEY_WII, NativeLibrary.IsEmulatingWii()) + } + fragment.arguments = arguments + return fragment + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SaveLoadStateFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SaveLoadStateFragment.java deleted file mode 100644 index 2afce4d1cf..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SaveLoadStateFragment.java +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.fragments; - -import android.os.Bundle; -import android.text.format.DateUtils; -import android.util.SparseIntArray; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Button; -import android.widget.GridLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; - -import org.dolphinemu.dolphinemu.NativeLibrary; -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.activities.EmulationActivity; -import org.dolphinemu.dolphinemu.databinding.FragmentSaveloadStateBinding; - -public final class SaveLoadStateFragment extends Fragment implements View.OnClickListener -{ - public enum SaveOrLoad - { - SAVE, LOAD - } - - private static final String KEY_SAVEORLOAD = "saveorload"; - - private static int[] saveActionsMap = new int[]{ - EmulationActivity.MENU_ACTION_SAVE_SLOT1, - EmulationActivity.MENU_ACTION_SAVE_SLOT2, - EmulationActivity.MENU_ACTION_SAVE_SLOT3, - EmulationActivity.MENU_ACTION_SAVE_SLOT4, - EmulationActivity.MENU_ACTION_SAVE_SLOT5, - EmulationActivity.MENU_ACTION_SAVE_SLOT6, - }; - - private static int[] loadActionsMap = new int[]{ - EmulationActivity.MENU_ACTION_LOAD_SLOT1, - EmulationActivity.MENU_ACTION_LOAD_SLOT2, - EmulationActivity.MENU_ACTION_LOAD_SLOT3, - EmulationActivity.MENU_ACTION_LOAD_SLOT4, - EmulationActivity.MENU_ACTION_LOAD_SLOT5, - EmulationActivity.MENU_ACTION_LOAD_SLOT6, - }; - - private static SparseIntArray buttonsMap = new SparseIntArray(); - - static - { - buttonsMap.append(R.id.loadsave_state_button_1, 0); - buttonsMap.append(R.id.loadsave_state_button_2, 1); - buttonsMap.append(R.id.loadsave_state_button_3, 2); - buttonsMap.append(R.id.loadsave_state_button_4, 3); - buttonsMap.append(R.id.loadsave_state_button_5, 4); - buttonsMap.append(R.id.loadsave_state_button_6, 5); - } - - private SaveOrLoad mSaveOrLoad; - - private FragmentSaveloadStateBinding mBinding; - - public static SaveLoadStateFragment newInstance(SaveOrLoad saveOrLoad) - { - SaveLoadStateFragment fragment = new SaveLoadStateFragment(); - - Bundle arguments = new Bundle(); - arguments.putSerializable(KEY_SAVEORLOAD, saveOrLoad); - fragment.setArguments(arguments); - - return fragment; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - - mSaveOrLoad = (SaveOrLoad) getArguments().getSerializable(KEY_SAVEORLOAD); - } - - @NonNull - @Override - public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) - { - mBinding = FragmentSaveloadStateBinding.inflate(inflater, container, false); - return mBinding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) - { - GridLayout grid = mBinding.gridStateSlots; - for (int childIndex = 0; childIndex < grid.getChildCount(); childIndex++) - { - Button button = (Button) grid.getChildAt(childIndex); - setButtonText(button, childIndex); - button.setOnClickListener(this); - } - - // So that item clicked to start this Fragment is no longer the focused item. - grid.requestFocus(); - } - - @Override - public void onDestroyView() - { - super.onDestroyView(); - mBinding = null; - } - - @Override - public void onClick(View view) - { - int buttonIndex = buttonsMap.get(view.getId(), -1); - - int action = (mSaveOrLoad == SaveOrLoad.SAVE ? saveActionsMap : loadActionsMap)[buttonIndex]; - ((EmulationActivity) getActivity()).handleMenuAction(action); - - if (mSaveOrLoad == SaveOrLoad.SAVE) - { - // Update the "last modified" time. - // The savestate most likely hasn't gotten saved to disk yet (it happens asynchronously), - // so we unfortunately can't rely on setButtonText/GetUnixTimeOfStateSlot here. - - Button button = (Button) view; - CharSequence time = DateUtils.getRelativeTimeSpanString(0, 0, DateUtils.MINUTE_IN_MILLIS); - button.setText(getString(R.string.emulation_state_slot, buttonIndex + 1, time)); - } - } - - private void setButtonText(Button button, int index) - { - long creationTime = NativeLibrary.GetUnixTimeOfStateSlot(index); - if (creationTime != 0) - { - CharSequence relativeTime = DateUtils.getRelativeTimeSpanString(creationTime); - button.setText(getString(R.string.emulation_state_slot, index + 1, relativeTime)); - } - else - { - button.setText(getString(R.string.emulation_state_slot_empty, index + 1)); - } - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SaveLoadStateFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SaveLoadStateFragment.kt new file mode 100644 index 0000000000..8ce2ccde10 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SaveLoadStateFragment.kt @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.fragments + +import android.os.Bundle +import android.text.format.DateUtils +import android.util.SparseIntArray +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import androidx.fragment.app.Fragment +import org.dolphinemu.dolphinemu.NativeLibrary +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.activities.EmulationActivity +import org.dolphinemu.dolphinemu.databinding.FragmentSaveloadStateBinding +import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable + +class SaveLoadStateFragment : Fragment(), View.OnClickListener { + enum class SaveOrLoad { SAVE, LOAD } + + private var saveOrLoad: SaveOrLoad? = null + + private var _binding: FragmentSaveloadStateBinding? = null + private val binding get() = _binding!! + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + saveOrLoad = requireArguments().serializable(KEY_SAVEORLOAD) as SaveOrLoad? + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + _binding = FragmentSaveloadStateBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val grid = binding.gridStateSlots + for (childIndex in 0 until grid.childCount) { + val button = grid.getChildAt(childIndex) as Button + setButtonText(button, childIndex) + button.setOnClickListener(this) + } + + // So that item clicked to start this Fragment is no longer the focused item. + grid.requestFocus() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + override fun onClick(view: View) { + val buttonIndex = buttonsMap[view.id, -1] + + val action = + (if (saveOrLoad == SaveOrLoad.SAVE) saveActionsMap else loadActionsMap)[buttonIndex] + (requireActivity() as EmulationActivity?)?.handleMenuAction(action) + + if (saveOrLoad == SaveOrLoad.SAVE) { + // Update the "last modified" time. + // The savestate most likely hasn't gotten saved to disk yet (it happens asynchronously), + // so we unfortunately can't rely on setButtonText/GetUnixTimeOfStateSlot here. + val button = view as Button + val time = DateUtils.getRelativeTimeSpanString(0, 0, DateUtils.MINUTE_IN_MILLIS) + button.text = getString(R.string.emulation_state_slot, buttonIndex + 1, time) + } + } + + private fun setButtonText(button: Button, index: Int) { + val creationTime = NativeLibrary.GetUnixTimeOfStateSlot(index) + button.text = if (creationTime != 0L) { + val relativeTime = DateUtils.getRelativeTimeSpanString(creationTime) + getString(R.string.emulation_state_slot, index + 1, relativeTime) + } else { + getString(R.string.emulation_state_slot_empty, index + 1) + } + } + + companion object { + private const val KEY_SAVEORLOAD = "saveorload" + + private val saveActionsMap = intArrayOf( + EmulationActivity.MENU_ACTION_SAVE_SLOT1, + EmulationActivity.MENU_ACTION_SAVE_SLOT2, + EmulationActivity.MENU_ACTION_SAVE_SLOT3, + EmulationActivity.MENU_ACTION_SAVE_SLOT4, + EmulationActivity.MENU_ACTION_SAVE_SLOT5, + EmulationActivity.MENU_ACTION_SAVE_SLOT6 + ) + + private val loadActionsMap = intArrayOf( + EmulationActivity.MENU_ACTION_LOAD_SLOT1, + EmulationActivity.MENU_ACTION_LOAD_SLOT2, + EmulationActivity.MENU_ACTION_LOAD_SLOT3, + EmulationActivity.MENU_ACTION_LOAD_SLOT4, + EmulationActivity.MENU_ACTION_LOAD_SLOT5, + EmulationActivity.MENU_ACTION_LOAD_SLOT6 + ) + + private val buttonsMap = SparseIntArray() + + init { + buttonsMap.append(R.id.loadsave_state_button_1, 0) + buttonsMap.append(R.id.loadsave_state_button_2, 1) + buttonsMap.append(R.id.loadsave_state_button_3, 2) + buttonsMap.append(R.id.loadsave_state_button_4, 3) + buttonsMap.append(R.id.loadsave_state_button_5, 4) + buttonsMap.append(R.id.loadsave_state_button_6, 5) + } + + fun newInstance(saveOrLoad: SaveOrLoad): SaveLoadStateFragment { + val fragment = SaveLoadStateFragment() + val arguments = Bundle() + arguments.putSerializable(KEY_SAVEORLOAD, saveOrLoad) + fragment.arguments = arguments + return fragment + } + } +}