Android: Convert SettingsAdapter to Kotlin

This commit is contained in:
Charles Lombardo 2023-03-15 15:33:53 -04:00
parent 673c8d9cb2
commit 9020b6aeb9
2 changed files with 590 additions and 703 deletions

View File

@ -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<SettingViewHolder>
implements DialogInterface.OnClickListener, Slider.OnChangeListener
{
private final SettingsFragmentView mView;
private final Context mContext;
private ArrayList<SettingsItem> 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<SettingsItem> 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<Long> 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;
}
}

View File

@ -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<SettingViewHolder>(), DialogInterface.OnClickListener,
Slider.OnChangeListener {
private var settingsList: ArrayList<SettingsItem>? = 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<SettingsItem>?) {
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)
}
}
}
}