diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java deleted file mode 100644 index e062dd71b2..0000000000 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java +++ /dev/null @@ -1,703 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later - -package org.dolphinemu.dolphinemu.features.settings.ui; - -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.provider.DocumentsContract; -import android.text.format.DateFormat; -import android.view.LayoutInflater; -import android.view.ViewGroup; -import android.widget.TextView; - -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.core.content.ContextCompat; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.color.MaterialColors; -import com.google.android.material.datepicker.CalendarConstraints; -import com.google.android.material.datepicker.MaterialDatePicker; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.elevation.ElevationOverlayProvider; -import com.google.android.material.slider.Slider; -import com.google.android.material.textfield.TextInputEditText; -import com.google.android.material.timepicker.MaterialTimePicker; -import com.google.android.material.timepicker.TimeFormat; - -import org.dolphinemu.dolphinemu.R; -import org.dolphinemu.dolphinemu.databinding.DialogAdvancedMappingBinding; -import org.dolphinemu.dolphinemu.databinding.DialogInputStringBinding; -import org.dolphinemu.dolphinemu.databinding.DialogSliderBinding; -import org.dolphinemu.dolphinemu.databinding.ListItemHeaderBinding; -import org.dolphinemu.dolphinemu.databinding.ListItemMappingBinding; -import org.dolphinemu.dolphinemu.databinding.ListItemSettingBinding; -import org.dolphinemu.dolphinemu.databinding.ListItemSettingSwitchBinding; -import org.dolphinemu.dolphinemu.databinding.ListItemSubmenuBinding; -import org.dolphinemu.dolphinemu.features.input.ui.AdvancedMappingDialog; -import org.dolphinemu.dolphinemu.features.input.ui.MotionAlertDialog; -import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting; -import org.dolphinemu.dolphinemu.features.input.ui.viewholder.InputMappingControlSettingViewHolder; -import org.dolphinemu.dolphinemu.features.settings.model.Settings; -import org.dolphinemu.dolphinemu.features.settings.model.view.DateTimeChoiceSetting; -import org.dolphinemu.dolphinemu.features.settings.model.view.SwitchSetting; -import org.dolphinemu.dolphinemu.features.settings.model.view.FilePicker; -import org.dolphinemu.dolphinemu.features.settings.model.view.FloatSliderSetting; -import org.dolphinemu.dolphinemu.features.settings.model.view.IntSliderSetting; -import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem; -import org.dolphinemu.dolphinemu.features.settings.model.view.SingleChoiceSetting; -import org.dolphinemu.dolphinemu.features.settings.model.view.SingleChoiceSettingDynamicDescriptions; -import org.dolphinemu.dolphinemu.features.settings.model.view.SliderSetting; -import org.dolphinemu.dolphinemu.features.settings.model.view.InputStringSetting; -import org.dolphinemu.dolphinemu.features.settings.model.view.StringSingleChoiceSetting; -import org.dolphinemu.dolphinemu.features.settings.model.view.SubmenuSetting; -import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.DateTimeSettingViewHolder; -import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.FilePickerViewHolder; -import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.HeaderHyperLinkViewHolder; -import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.HeaderViewHolder; -import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.InputStringSettingViewHolder; -import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.RunRunnableViewHolder; -import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHolder; -import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SingleChoiceViewHolder; -import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SliderViewHolder; -import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SubmenuViewHolder; -import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SwitchSettingViewHolder; -import org.dolphinemu.dolphinemu.utils.DirectoryInitialization; -import org.dolphinemu.dolphinemu.utils.FileBrowserHelper; -import org.dolphinemu.dolphinemu.utils.Log; -import org.dolphinemu.dolphinemu.utils.PermissionsHandler; - -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.TimeZone; - -public final class SettingsAdapter extends RecyclerView.Adapter - implements DialogInterface.OnClickListener, Slider.OnChangeListener -{ - private final SettingsFragmentView mView; - private final Context mContext; - private ArrayList mSettings; - - private SettingsItem mClickedItem; - private int mClickedPosition; - private int mSeekbarProgress; - - private AlertDialog mDialog; - private TextView mTextSliderValue; - - public SettingsAdapter(SettingsFragmentView view, Context context) - { - mView = view; - mContext = context; - mClickedPosition = -1; - } - - @NonNull - @Override - public SettingViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) - { - LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - switch (viewType) - { - case SettingsItem.TYPE_HEADER: - return new HeaderViewHolder(ListItemHeaderBinding.inflate(inflater), this); - - case SettingsItem.TYPE_SWITCH: - return new SwitchSettingViewHolder(ListItemSettingSwitchBinding.inflate(inflater), - this); - - case SettingsItem.TYPE_STRING_SINGLE_CHOICE: - case SettingsItem.TYPE_SINGLE_CHOICE_DYNAMIC_DESCRIPTIONS: - case SettingsItem.TYPE_SINGLE_CHOICE: - return new SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this); - - case SettingsItem.TYPE_SLIDER: - return new SliderViewHolder(ListItemSettingBinding.inflate(inflater), this, mContext); - - case SettingsItem.TYPE_SUBMENU: - return new SubmenuViewHolder(ListItemSubmenuBinding.inflate(inflater), this); - - case SettingsItem.TYPE_INPUT_MAPPING_CONTROL: - return new InputMappingControlSettingViewHolder(ListItemMappingBinding.inflate(inflater), - this); - - case SettingsItem.TYPE_FILE_PICKER: - return new FilePickerViewHolder(ListItemSettingBinding.inflate(inflater), this); - - case SettingsItem.TYPE_RUN_RUNNABLE: - return new RunRunnableViewHolder(ListItemSettingBinding.inflate(inflater), this, mContext); - - case SettingsItem.TYPE_STRING: - return new InputStringSettingViewHolder(ListItemSettingBinding.inflate(inflater), this); - - case SettingsItem.TYPE_HYPERLINK_HEADER: - return new HeaderHyperLinkViewHolder(ListItemHeaderBinding.inflate(inflater), this); - - case SettingsItem.TYPE_DATETIME_CHOICE: - return new DateTimeSettingViewHolder(ListItemSettingBinding.inflate(inflater), this); - - default: - throw new IllegalArgumentException("Invalid view type: " + viewType); - } - } - - @Override - public void onBindViewHolder(@NonNull SettingViewHolder holder, int position) - { - holder.bind(getItem(position)); - } - - private SettingsItem getItem(int position) - { - return mSettings.get(position); - } - - @Override - public int getItemCount() - { - if (mSettings != null) - { - return mSettings.size(); - } - else - { - return 0; - } - } - - @Override - public int getItemViewType(int position) - { - return getItem(position).getType(); - } - - public Settings getSettings() - { - return mView.getSettings(); - } - - public void setSettings(ArrayList settings) - { - mSettings = settings; - notifyDataSetChanged(); - } - - public void clearSetting(SettingsItem item) - { - item.clear(getSettings()); - - mView.onSettingChanged(); - } - - public void notifyAllSettingsChanged() - { - notifyItemRangeChanged(0, getItemCount()); - - mView.onSettingChanged(); - } - - public void onBooleanClick(SwitchSetting item, boolean checked) - { - item.setChecked(getSettings(), checked); - - mView.onSettingChanged(); - } - - public void onInputStringClick(InputStringSetting item, int position) - { - LayoutInflater inflater = LayoutInflater.from(mContext); - - DialogInputStringBinding binding = DialogInputStringBinding.inflate(inflater); - TextInputEditText input = binding.input; - input.setText(item.getSelectedValue()); - - mDialog = new MaterialAlertDialogBuilder(mView.getActivity()) - .setView(binding.getRoot()) - .setMessage(item.getDescription()) - .setPositiveButton(R.string.ok, (dialogInterface, i) -> - { - String editTextInput = input.getText().toString(); - - if (!item.getSelectedValue().equals(editTextInput)) - { - notifyItemChanged(position); - mView.onSettingChanged(); - } - - item.setSelectedValue(mView.getSettings(), editTextInput); - }) - .setNegativeButton(R.string.cancel, null) - .show(); - } - - public void onSingleChoiceClick(SingleChoiceSetting item, int position) - { - mClickedItem = item; - mClickedPosition = position; - - int value = getSelectionForSingleChoiceValue(item); - - mDialog = new MaterialAlertDialogBuilder(mView.getActivity()) - .setTitle(item.getName()) - .setSingleChoiceItems(item.getChoicesId(), value, this) - .show(); - } - - public void onStringSingleChoiceClick(StringSingleChoiceSetting item, int position) - { - mClickedItem = item; - mClickedPosition = position; - - item.refreshChoicesAndValues(); - - String[] choices = item.getChoices(); - int noChoicesAvailableString = item.getNoChoicesAvailableString(); - if (noChoicesAvailableString != 0 && choices.length == 0) - { - mDialog = new MaterialAlertDialogBuilder(mView.getActivity()) - .setTitle(item.getName()) - .setMessage(noChoicesAvailableString) - .setPositiveButton(R.string.ok, null) - .show(); - } - else - { - mDialog = new MaterialAlertDialogBuilder(mView.getActivity()) - .setTitle(item.getName()) - .setSingleChoiceItems(item.getChoices(), item.getSelectedValueIndex(), - this) - .show(); - } - } - - public void onSingleChoiceDynamicDescriptionsClick(SingleChoiceSettingDynamicDescriptions item, - int position) - { - mClickedItem = item; - mClickedPosition = position; - - int value = getSelectionForSingleChoiceDynamicDescriptionsValue(item); - - mDialog = new MaterialAlertDialogBuilder(mView.getActivity()) - .setTitle(item.getName()) - .setSingleChoiceItems(item.getChoicesId(), value, this) - .show(); - } - - public void onSliderClick(SliderSetting item, int position) - { - mClickedItem = item; - mClickedPosition = position; - mSeekbarProgress = item.getSelectedValue(); - - LayoutInflater inflater = LayoutInflater.from(mView.getActivity()); - DialogSliderBinding binding = DialogSliderBinding.inflate(inflater); - - mTextSliderValue = binding.textValue; - mTextSliderValue.setText(String.valueOf(mSeekbarProgress)); - - binding.textUnits.setText(item.getUnits()); - - Slider slider = binding.slider; - slider.setValueFrom(item.getMin()); - slider.setValueTo(item.getMax()); - slider.setValue(mSeekbarProgress); - slider.setStepSize(item.getStepSize()); - - slider.addOnChangeListener(this); - - mDialog = new MaterialAlertDialogBuilder(mView.getActivity()) - .setTitle(item.getName()) - .setView(binding.getRoot()) - .setPositiveButton(R.string.ok, this) - .show(); - } - - public void onSubmenuClick(SubmenuSetting item) - { - mView.loadSubMenu(item.getMenuKey()); - } - - public void onInputMappingClick(final InputMappingControlSetting item, final int position) - { - if (item.getController().getDefaultDevice().isEmpty() && !mView.isMappingAllDevices()) - { - new MaterialAlertDialogBuilder(mView.getActivity()) - .setMessage(R.string.input_binding_no_device) - .setPositiveButton(R.string.ok, this) - .show(); - return; - } - - final MotionAlertDialog dialog = new MotionAlertDialog(mView.getActivity(), item, - mView.isMappingAllDevices()); - - Drawable background = ContextCompat.getDrawable(mContext, R.drawable.dialog_round); - @ColorInt int color = new ElevationOverlayProvider(dialog.getContext()).compositeOverlay( - MaterialColors.getColor(dialog.getWindow().getDecorView(), R.attr.colorSurface), - dialog.getWindow().getDecorView().getElevation()); - background.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); - dialog.getWindow().setBackgroundDrawable(background); - - dialog.setTitle(R.string.input_binding); - dialog.setMessage(String.format(mContext.getString(R.string.input_binding_description), - item.getName())); - dialog.setButton(AlertDialog.BUTTON_NEGATIVE, mContext.getString(R.string.cancel), this); - dialog.setButton(AlertDialog.BUTTON_NEUTRAL, mContext.getString(R.string.clear), - (dialogInterface, i) -> item.clearValue()); - dialog.setOnDismissListener(dialog1 -> - { - notifyItemChanged(position); - mView.onSettingChanged(); - }); - dialog.setCanceledOnTouchOutside(false); - dialog.show(); - } - - public void onAdvancedInputMappingClick(final InputMappingControlSetting item, final int position) - { - LayoutInflater inflater = LayoutInflater.from(mContext); - - DialogAdvancedMappingBinding binding = DialogAdvancedMappingBinding.inflate(inflater); - - final AdvancedMappingDialog dialog = new AdvancedMappingDialog(mContext, binding, - item.getControlReference(), item.getController()); - - Drawable background = ContextCompat.getDrawable(mContext, R.drawable.dialog_round); - @ColorInt int color = new ElevationOverlayProvider(dialog.getContext()).compositeOverlay( - MaterialColors.getColor(dialog.getWindow().getDecorView(), R.attr.colorSurface), - dialog.getWindow().getDecorView().getElevation()); - background.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); - dialog.getWindow().setBackgroundDrawable(background); - - dialog.setTitle(item.isInput() ? - R.string.input_configure_input : R.string.input_configure_output); - dialog.setView(binding.getRoot()); - dialog.setButton(AlertDialog.BUTTON_POSITIVE, mContext.getString(R.string.ok), - (dialogInterface, i) -> - { - item.setValue(dialog.getExpression()); - notifyItemChanged(position); - mView.onSettingChanged(); - }); - dialog.setButton(AlertDialog.BUTTON_NEGATIVE, mContext.getString(R.string.cancel), this); - dialog.setButton(AlertDialog.BUTTON_NEUTRAL, mContext.getString(R.string.clear), - (dialogInterface, i) -> - { - item.clearValue(); - notifyItemChanged(position); - mView.onSettingChanged(); - }); - dialog.setCanceledOnTouchOutside(false); - dialog.show(); - } - - public void onFilePickerDirectoryClick(SettingsItem item, int position) - { - mClickedItem = item; - mClickedPosition = position; - - if (!PermissionsHandler.isExternalStorageLegacy()) - { - new MaterialAlertDialogBuilder(mContext) - .setMessage(R.string.path_not_changeable_scoped_storage) - .setPositiveButton(R.string.ok, (dialog, i) -> dialog.dismiss()) - .show(); - } - else - { - FileBrowserHelper.openDirectoryPicker(mView.getActivity(), FileBrowserHelper.GAME_EXTENSIONS); - } - } - - public void onFilePickerFileClick(SettingsItem item, int position) - { - mClickedItem = item; - mClickedPosition = position; - FilePicker filePicker = (FilePicker) item; - - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("*/*"); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - { - intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, filePicker.getSelectedValue()); - } - - mView.getActivity().startActivityForResult(intent, filePicker.getRequestType()); - } - - public void onDateTimeClick(DateTimeChoiceSetting item, int position) - { - mClickedItem = item; - mClickedPosition = position; - long storedTime = Long.decode(item.getSelectedValue()) * 1000; - - // Helper to extract hour and minute from epoch time - Calendar calendar = Calendar.getInstance(); - calendar.setTimeInMillis(storedTime); - calendar.setTimeZone(TimeZone.getTimeZone("UTC")); - - // Start and end epoch times available for the Wii's date picker - CalendarConstraints calendarConstraints = new CalendarConstraints.Builder() - .setStart(946684800000L) - .setEnd(2082672000000L) - .build(); - - int timeFormat = TimeFormat.CLOCK_12H; - if (DateFormat.is24HourFormat(mView.getActivity())) - { - timeFormat = TimeFormat.CLOCK_24H; - } - - MaterialDatePicker datePicker = MaterialDatePicker.Builder.datePicker() - .setSelection(storedTime) - .setTitleText(R.string.select_rtc_date) - .setCalendarConstraints(calendarConstraints) - .build(); - MaterialTimePicker timePicker = new MaterialTimePicker.Builder() - .setTimeFormat(timeFormat) - .setHour(calendar.get(Calendar.HOUR_OF_DAY)) - .setMinute(calendar.get(Calendar.MINUTE)) - .setTitleText(R.string.select_rtc_time) - .build(); - - datePicker.addOnPositiveButtonClickListener( - selection -> timePicker.show(mView.getActivity().getSupportFragmentManager(), - "TimePicker")); - timePicker.addOnPositiveButtonClickListener(selection -> - { - long epochTime = datePicker.getSelection() / 1000; - epochTime += (long) timePicker.getHour() * 60 * 60; - epochTime += (long) timePicker.getMinute() * 60; - String rtcString = "0x" + Long.toHexString(epochTime); - if (!item.getSelectedValue().equals(rtcString)) - { - notifyItemChanged(mClickedPosition); - mView.onSettingChanged(); - } - - item.setSelectedValue(mView.getSettings(), rtcString); - - mClickedItem = null; - }); - datePicker.show(mView.getActivity().getSupportFragmentManager(), "DatePicker"); - } - - public void onFilePickerConfirmation(String selectedFile) - { - FilePicker filePicker = (FilePicker) mClickedItem; - - if (!filePicker.getSelectedValue().equals(selectedFile)) - { - notifyItemChanged(mClickedPosition); - mView.onSettingChanged(); - } - - filePicker.setSelectedValue(mView.getSettings(), selectedFile); - - mClickedItem = null; - } - - public static void clearLog() - { - // Don't delete the log in case it is being monitored by another app. - File log = new File(DirectoryInitialization.getUserDirectory() + "/Logs/dolphin.log"); - - try - { - RandomAccessFile raf = new RandomAccessFile(log, "rw"); - raf.setLength(0); - } - catch (IOException e) - { - Log.error("[SettingsAdapter] Failed to clear log file: " + e.getMessage()); - } - } - - public void onMenuTagAction(@NonNull MenuTag menuTag, int value) - { - mView.onMenuTagAction(menuTag, value); - } - - public boolean hasMenuTagActionForValue(@NonNull MenuTag menuTag, int value) - { - return mView.hasMenuTagActionForValue(menuTag, value); - } - - @Override - public void onClick(DialogInterface dialog, int which) - { - if (mClickedItem instanceof SingleChoiceSetting) - { - SingleChoiceSetting scSetting = (SingleChoiceSetting) mClickedItem; - - int value = getValueForSingleChoiceSelection(scSetting, which); - if (scSetting.getSelectedValue() != value) - mView.onSettingChanged(); - - scSetting.setSelectedValue(getSettings(), value); - - closeDialog(); - } - else if (mClickedItem instanceof SingleChoiceSettingDynamicDescriptions) - { - SingleChoiceSettingDynamicDescriptions scSetting = - (SingleChoiceSettingDynamicDescriptions) mClickedItem; - - int value = getValueForSingleChoiceDynamicDescriptionsSelection(scSetting, which); - if (scSetting.getSelectedValue() != value) - mView.onSettingChanged(); - - scSetting.setSelectedValue(getSettings(), value); - - closeDialog(); - } - else if (mClickedItem instanceof StringSingleChoiceSetting) - { - StringSingleChoiceSetting scSetting = (StringSingleChoiceSetting) mClickedItem; - String value = scSetting.getValueAt(which); - if (!scSetting.getSelectedValue().equals(value)) - mView.onSettingChanged(); - - scSetting.setSelectedValue(getSettings(), value); - - closeDialog(); - } - else if (mClickedItem instanceof IntSliderSetting) - { - IntSliderSetting sliderSetting = (IntSliderSetting) mClickedItem; - if (sliderSetting.getSelectedValue() != mSeekbarProgress) - mView.onSettingChanged(); - - sliderSetting.setSelectedValue(getSettings(), mSeekbarProgress); - - closeDialog(); - } - else if (mClickedItem instanceof FloatSliderSetting) - { - FloatSliderSetting sliderSetting = (FloatSliderSetting) mClickedItem; - if (sliderSetting.getSelectedValue() != mSeekbarProgress) - mView.onSettingChanged(); - - sliderSetting.setSelectedValue(getSettings(), mSeekbarProgress); - - closeDialog(); - } - - mClickedItem = null; - mSeekbarProgress = -1; - } - - public void closeDialog() - { - if (mDialog != null) - { - if (mClickedPosition != -1) - { - notifyItemChanged(mClickedPosition); - mClickedPosition = -1; - } - mDialog.dismiss(); - mDialog = null; - } - } - - @Override - public void onValueChange(@NonNull Slider slider, float progress, boolean fromUser) - { - mSeekbarProgress = (int) progress; - mTextSliderValue.setText(String.valueOf(mSeekbarProgress)); - } - - private int getValueForSingleChoiceSelection(SingleChoiceSetting item, int which) - { - int valuesId = item.getValuesId(); - - if (valuesId > 0) - { - int[] valuesArray = mContext.getResources().getIntArray(valuesId); - return valuesArray[which]; - } - else - { - return which; - } - } - - private int getSelectionForSingleChoiceValue(SingleChoiceSetting item) - { - int value = item.getSelectedValue(); - int valuesId = item.getValuesId(); - - if (valuesId > 0) - { - int[] valuesArray = mContext.getResources().getIntArray(valuesId); - for (int index = 0; index < valuesArray.length; index++) - { - int current = valuesArray[index]; - if (current == value) - { - return index; - } - } - } - else - { - return value; - } - - return -1; - } - - private int getValueForSingleChoiceDynamicDescriptionsSelection( - SingleChoiceSettingDynamicDescriptions item, int which) - { - int valuesId = item.getValuesId(); - - if (valuesId > 0) - { - int[] valuesArray = mContext.getResources().getIntArray(valuesId); - return valuesArray[which]; - } - else - { - return which; - } - } - - private int getSelectionForSingleChoiceDynamicDescriptionsValue( - SingleChoiceSettingDynamicDescriptions item) - { - int value = item.getSelectedValue(); - int valuesId = item.getValuesId(); - - if (valuesId > 0) - { - int[] valuesArray = mContext.getResources().getIntArray(valuesId); - for (int index = 0; index < valuesArray.length; index++) - { - int current = valuesArray[index]; - if (current == value) - { - return index; - } - } - } - else - { - return value; - } - - return -1; - } -} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt new file mode 100644 index 0000000000..d5df7c0e09 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt @@ -0,0 +1,590 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.features.settings.ui + +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.graphics.PorterDuff +import android.os.Build +import android.provider.DocumentsContract +import android.text.format.DateFormat +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.color.MaterialColors +import com.google.android.material.datepicker.CalendarConstraints +import com.google.android.material.datepicker.MaterialDatePicker +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.elevation.ElevationOverlayProvider +import com.google.android.material.slider.Slider +import com.google.android.material.timepicker.MaterialTimePicker +import com.google.android.material.timepicker.TimeFormat +import org.dolphinemu.dolphinemu.R +import org.dolphinemu.dolphinemu.databinding.* +import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting +import org.dolphinemu.dolphinemu.features.input.ui.AdvancedMappingDialog +import org.dolphinemu.dolphinemu.features.input.ui.MotionAlertDialog +import org.dolphinemu.dolphinemu.features.input.ui.viewholder.InputMappingControlSettingViewHolder +import org.dolphinemu.dolphinemu.features.settings.model.Settings +import org.dolphinemu.dolphinemu.features.settings.model.view.* +import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.* +import org.dolphinemu.dolphinemu.utils.DirectoryInitialization +import org.dolphinemu.dolphinemu.utils.FileBrowserHelper +import org.dolphinemu.dolphinemu.utils.Log +import org.dolphinemu.dolphinemu.utils.PermissionsHandler +import java.io.File +import java.io.IOException +import java.io.RandomAccessFile +import java.util.* + +class SettingsAdapter( + private val fragmentView: SettingsFragmentView, + private val context: Context +) : + RecyclerView.Adapter(), DialogInterface.OnClickListener, + Slider.OnChangeListener { + private var settingsList: ArrayList? = null + private var clickedItem: SettingsItem? = null + private var clickedPosition: Int = -1 + private var seekbarProgress = 0 + private var dialog: AlertDialog? = null + private var textSliderValue: TextView? = null + + val settings: Settings? + get() = fragmentView.settings + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + SettingsItem.TYPE_HEADER -> HeaderViewHolder( + ListItemHeaderBinding.inflate(inflater), + this + ) + SettingsItem.TYPE_SWITCH -> SwitchSettingViewHolder( + ListItemSettingSwitchBinding.inflate(inflater), + this + ) + SettingsItem.TYPE_STRING_SINGLE_CHOICE, + SettingsItem.TYPE_SINGLE_CHOICE_DYNAMIC_DESCRIPTIONS, + SettingsItem.TYPE_SINGLE_CHOICE -> SingleChoiceViewHolder( + ListItemSettingBinding.inflate(inflater), + this + ) + SettingsItem.TYPE_SLIDER -> SliderViewHolder( + ListItemSettingBinding.inflate( + inflater + ), this, context + ) + SettingsItem.TYPE_SUBMENU -> SubmenuViewHolder( + ListItemSubmenuBinding.inflate( + inflater + ), this + ) + SettingsItem.TYPE_INPUT_MAPPING_CONTROL -> InputMappingControlSettingViewHolder( + ListItemMappingBinding.inflate(inflater), + this + ) + SettingsItem.TYPE_FILE_PICKER -> FilePickerViewHolder( + ListItemSettingBinding.inflate( + inflater + ), this + ) + SettingsItem.TYPE_RUN_RUNNABLE -> RunRunnableViewHolder( + ListItemSettingBinding.inflate( + inflater + ), this, context + ) + SettingsItem.TYPE_STRING -> InputStringSettingViewHolder( + ListItemSettingBinding.inflate( + inflater + ), this + ) + SettingsItem.TYPE_HYPERLINK_HEADER -> HeaderHyperLinkViewHolder( + ListItemHeaderBinding.inflate( + inflater + ), this + ) + SettingsItem.TYPE_DATETIME_CHOICE -> DateTimeSettingViewHolder( + ListItemSettingBinding.inflate( + inflater + ), this + ) + else -> throw IllegalArgumentException("Invalid view type: $viewType") + } + } + + override fun onBindViewHolder(holder: SettingViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + private fun getItem(position: Int): SettingsItem { + return settingsList!![position] + } + + override fun getItemCount(): Int { + return if (settingsList != null) { + settingsList!!.size + } else { + 0 + } + } + + override fun getItemViewType(position: Int): Int { + return getItem(position).type + } + + fun setSettings(settings: ArrayList?) { + settingsList = settings + notifyDataSetChanged() + } + + fun clearSetting(item: SettingsItem) { + item.clear(settings!!) + fragmentView.onSettingChanged() + } + + fun notifyAllSettingsChanged() { + notifyItemRangeChanged(0, itemCount) + fragmentView.onSettingChanged() + } + + fun onBooleanClick(item: SwitchSetting, checked: Boolean) { + item.setChecked(settings, checked) + fragmentView.onSettingChanged() + } + + fun onInputStringClick(item: InputStringSetting, position: Int) { + val inflater = LayoutInflater.from(context) + val binding = DialogInputStringBinding.inflate(inflater) + val input = binding.input + input.setText(item.selectedValue) + dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity) + .setView(binding.root) + .setMessage(item.description) + .setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int -> + val editTextInput = input.text.toString() + if (item.selectedValue != editTextInput) { + notifyItemChanged(position) + fragmentView.onSettingChanged() + } + item.setSelectedValue(fragmentView.settings!!, editTextInput) + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) { + clickedItem = item + clickedPosition = position + val value = getSelectionForSingleChoiceValue(item) + dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity) + .setTitle(item.name) + .setSingleChoiceItems(item.choicesId, value, this) + .show() + } + + fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) { + clickedItem = item + clickedPosition = position + item.refreshChoicesAndValues() + val choices = item.choices + val noChoicesAvailableString = item.noChoicesAvailableString + dialog = if (noChoicesAvailableString != 0 && choices!!.isEmpty()) { + MaterialAlertDialogBuilder(fragmentView.fragmentActivity) + .setTitle(item.name) + .setMessage(noChoicesAvailableString) + .setPositiveButton(R.string.ok, null) + .show() + } else { + MaterialAlertDialogBuilder(fragmentView.fragmentActivity) + .setTitle(item.name) + .setSingleChoiceItems( + item.choices, item.selectedValueIndex, + this + ) + .show() + } + } + + fun onSingleChoiceDynamicDescriptionsClick( + item: SingleChoiceSettingDynamicDescriptions, + position: Int + ) { + clickedItem = item + clickedPosition = position + + val value = getSelectionForSingleChoiceDynamicDescriptionsValue(item) + + dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity) + .setTitle(item.name) + .setSingleChoiceItems(item.choicesId, value, this) + .show() + } + + fun onSliderClick(item: SliderSetting, position: Int) { + clickedItem = item + clickedPosition = position + seekbarProgress = item.selectedValue + + val inflater = LayoutInflater.from(fragmentView.fragmentActivity) + val binding = DialogSliderBinding.inflate(inflater) + + textSliderValue = binding.textValue + textSliderValue!!.text = seekbarProgress.toString() + + binding.textUnits.text = item.units + + val slider = binding.slider + slider.valueFrom = item.min.toFloat() + slider.valueTo = item.max.toFloat() + slider.value = seekbarProgress.toFloat() + slider.stepSize = item.stepSize.toFloat() + slider.addOnChangeListener(this) + + dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity) + .setTitle(item.name) + .setView(binding.root) + .setPositiveButton(R.string.ok, this) + .show() + } + + fun onSubmenuClick(item: SubmenuSetting) { + fragmentView.loadSubMenu(item.menuKey) + } + + fun onInputMappingClick(item: InputMappingControlSetting, position: Int) { + if (item.controller.defaultDevice.isEmpty() && !fragmentView.isMappingAllDevices) { + MaterialAlertDialogBuilder(fragmentView.fragmentActivity) + .setMessage(R.string.input_binding_no_device) + .setPositiveButton(R.string.ok, this) + .show() + return + } + + val dialog = MotionAlertDialog( + fragmentView.fragmentActivity, item, + fragmentView.isMappingAllDevices + ) + + val background = ContextCompat.getDrawable(context, R.drawable.dialog_round) + @ColorInt val color = ElevationOverlayProvider(dialog.context).compositeOverlay( + MaterialColors.getColor(dialog.window!!.decorView, R.attr.colorSurface), + dialog.window!!.decorView.elevation + ) + background!!.setColorFilter(color, PorterDuff.Mode.SRC_ATOP) + dialog.window!!.setBackgroundDrawable(background) + + dialog.setTitle(R.string.input_binding) + dialog.setMessage( + String.format( + context.getString(R.string.input_binding_description), + item.name + ) + ) + dialog.setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(R.string.cancel), this) + dialog.setButton( + AlertDialog.BUTTON_NEUTRAL, + context.getString(R.string.clear) + ) { _: DialogInterface?, _: Int -> item.clearValue() } + dialog.setOnDismissListener { + notifyItemChanged(position) + fragmentView.onSettingChanged() + } + dialog.setCanceledOnTouchOutside(false) + dialog.show() + } + + fun onAdvancedInputMappingClick(item: InputMappingControlSetting, position: Int) { + val inflater = LayoutInflater.from(context) + val binding = DialogAdvancedMappingBinding.inflate(inflater) + val dialog = AdvancedMappingDialog( + context, + binding, + item.controlReference, + item.controller + ) + + val background = ContextCompat.getDrawable(context, R.drawable.dialog_round) + @ColorInt val color = ElevationOverlayProvider(dialog.context).compositeOverlay( + MaterialColors.getColor(dialog.window!!.decorView, R.attr.colorSurface), + dialog.window!!.decorView.elevation + ) + background!!.setColorFilter(color, PorterDuff.Mode.SRC_ATOP) + dialog.window!!.setBackgroundDrawable(background) + + dialog.setTitle(if (item.isInput) R.string.input_configure_input else R.string.input_configure_output) + dialog.setView(binding.root) + dialog.setButton( + AlertDialog.BUTTON_POSITIVE, context.getString(R.string.ok) + ) { _: DialogInterface?, _: Int -> + item.value = dialog.expression + notifyItemChanged(position) + fragmentView.onSettingChanged() + } + dialog.setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(R.string.cancel), this) + dialog.setButton( + AlertDialog.BUTTON_NEUTRAL, + context.getString(R.string.clear) + ) { _: DialogInterface?, _: Int -> + item.clearValue() + notifyItemChanged(position) + fragmentView.onSettingChanged() + } + dialog.setCanceledOnTouchOutside(false) + dialog.show() + } + + fun onFilePickerDirectoryClick(item: SettingsItem?, position: Int) { + clickedItem = item + clickedPosition = position + + if (!PermissionsHandler.isExternalStorageLegacy()) { + MaterialAlertDialogBuilder(context) + .setMessage(R.string.path_not_changeable_scoped_storage) + .setPositiveButton(R.string.ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() } + .show() + } else { + FileBrowserHelper.openDirectoryPicker( + fragmentView.fragmentActivity, + FileBrowserHelper.GAME_EXTENSIONS + ) + } + } + + fun onFilePickerFileClick(item: SettingsItem, position: Int) { + clickedItem = item + clickedPosition = position + val filePicker = item as FilePicker + + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "*/*" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, filePicker.getSelectedValue()) + } + + fragmentView.fragmentActivity.startActivityForResult(intent, filePicker.requestType) + } + + fun onDateTimeClick(item: DateTimeChoiceSetting, position: Int) { + clickedItem = item + clickedPosition = position + val storedTime = java.lang.Long.decode(item.getSelectedValue()) * 1000 + + // Helper to extract hour and minute from epoch time + val calendar = Calendar.getInstance() + calendar.timeInMillis = storedTime + calendar.timeZone = TimeZone.getTimeZone("UTC") + + // Start and end epoch times available for the Wii's date picker + val calendarConstraints = CalendarConstraints.Builder() + .setStart(946684800000L) + .setEnd(2082672000000L) + .build() + + var timeFormat = TimeFormat.CLOCK_12H + if (DateFormat.is24HourFormat(fragmentView.fragmentActivity)) { + timeFormat = TimeFormat.CLOCK_24H + } + + val datePicker = MaterialDatePicker.Builder.datePicker() + .setSelection(storedTime) + .setTitleText(R.string.select_rtc_date) + .setCalendarConstraints(calendarConstraints) + .build() + val timePicker = MaterialTimePicker.Builder() + .setTimeFormat(timeFormat) + .setHour(calendar[Calendar.HOUR_OF_DAY]) + .setMinute(calendar[Calendar.MINUTE]) + .setTitleText(R.string.select_rtc_time) + .build() + + datePicker.addOnPositiveButtonClickListener { + timePicker.show( + fragmentView.fragmentActivity.supportFragmentManager, + "TimePicker" + ) + } + timePicker.addOnPositiveButtonClickListener { + var epochTime = datePicker.selection!! / 1000 + epochTime += timePicker.hour.toLong() * 60 * 60 + epochTime += timePicker.minute.toLong() * 60 + val rtcString = "0x" + java.lang.Long.toHexString(epochTime) + if (item.getSelectedValue() != rtcString) { + notifyItemChanged(clickedPosition) + fragmentView.onSettingChanged() + } + item.setSelectedValue(fragmentView.settings!!, rtcString) + clickedItem = null + } + datePicker.show(fragmentView.fragmentActivity.supportFragmentManager, "DatePicker") + } + + fun onFilePickerConfirmation(selectedFile: String) { + val filePicker = clickedItem as FilePicker? + + if (filePicker!!.getSelectedValue() != selectedFile) { + notifyItemChanged(clickedPosition) + fragmentView.onSettingChanged() + } + + filePicker.setSelectedValue(fragmentView.settings!!, selectedFile) + + clickedItem = null + } + + fun onMenuTagAction(menuTag: MenuTag, value: Int) { + fragmentView.onMenuTagAction(menuTag, value) + } + + fun hasMenuTagActionForValue(menuTag: MenuTag, value: Int): Boolean { + return fragmentView.hasMenuTagActionForValue(menuTag, value) + } + + override fun onClick(dialog: DialogInterface, which: Int) { + when (clickedItem) { + is SingleChoiceSetting -> { + val scSetting = clickedItem as SingleChoiceSetting + + val value = getValueForSingleChoiceSelection(scSetting, which) + if (scSetting.selectedValue != value) fragmentView.onSettingChanged() + + scSetting.setSelectedValue(settings, value) + + closeDialog() + } + is SingleChoiceSettingDynamicDescriptions -> { + val scSetting = clickedItem as SingleChoiceSettingDynamicDescriptions + + val value = getValueForSingleChoiceDynamicDescriptionsSelection(scSetting, which) + if (scSetting.selectedValue != value) fragmentView.onSettingChanged() + + scSetting.setSelectedValue(settings!!, value) + + closeDialog() + } + is StringSingleChoiceSetting -> { + val scSetting = clickedItem as StringSingleChoiceSetting + + val value = scSetting.getValueAt(which) + if (scSetting.selectedValue != value) fragmentView.onSettingChanged() + + scSetting.setSelectedValue(settings, value) + + closeDialog() + } + is IntSliderSetting -> { + val sliderSetting = clickedItem as IntSliderSetting + if (sliderSetting.selectedValue != seekbarProgress) fragmentView.onSettingChanged() + sliderSetting.setSelectedValue(settings, seekbarProgress) + closeDialog() + } + is FloatSliderSetting -> { + val sliderSetting = clickedItem as FloatSliderSetting + + if (sliderSetting.selectedValue != seekbarProgress) fragmentView.onSettingChanged() + + sliderSetting.setSelectedValue(settings, seekbarProgress.toFloat()) + + closeDialog() + } + } + clickedItem = null + seekbarProgress = -1 + } + + fun closeDialog() { + if (dialog != null) { + if (clickedPosition != -1) { + notifyItemChanged(clickedPosition) + clickedPosition = -1 + } + dialog!!.dismiss() + dialog = null + } + } + + override fun onValueChange(slider: Slider, progress: Float, fromUser: Boolean) { + seekbarProgress = progress.toInt() + textSliderValue!!.text = seekbarProgress.toString() + } + + private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int { + val valuesId = item.valuesId + + return if (valuesId > 0) { + val valuesArray = context.resources.getIntArray(valuesId) + valuesArray[which] + } else { + which + } + } + + private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int { + val value = item.selectedValue + val valuesId = item.valuesId + + if (valuesId > 0) { + val valuesArray = context.resources.getIntArray(valuesId) + for (index in valuesArray.indices) { + val current = valuesArray[index] + if (current == value) { + return index + } + } + } else { + return value + } + return -1 + } + + private fun getValueForSingleChoiceDynamicDescriptionsSelection( + item: SingleChoiceSettingDynamicDescriptions, + which: Int + ): Int { + val valuesId = item.valuesId + return if (valuesId > 0) { + val valuesArray = context.resources.getIntArray(valuesId) + valuesArray[which] + } else { + which + } + } + + private fun getSelectionForSingleChoiceDynamicDescriptionsValue( + item: SingleChoiceSettingDynamicDescriptions + ): Int { + val value = item.selectedValue + val valuesId = item.valuesId + if (valuesId > 0) { + val valuesArray = context.resources.getIntArray(valuesId) + for (index in valuesArray.indices) { + val current = valuesArray[index] + if (current == value) { + return index + } + } + } else { + return value + } + return -1 + } + + companion object { + fun clearLog() { + // Don't delete the log in case it is being monitored by another app. + val log = File(DirectoryInitialization.getUserDirectory() + "/Logs/dolphin.log") + try { + val raf = RandomAccessFile(log, "rw") + raf.setLength(0) + } catch (e: IOException) { + Log.error("[SettingsAdapter] Failed to clear log file: " + e.message) + } + } + } +}