Split most classes into separate files.

Non-activity class files are placed into packages named after the
associated activity.
This commit is contained in:
Phil Jones 2022-04-29 16:24:50 +01:00
parent 0fcbc8521a
commit 64b731af6b
No known key found for this signature in database
GPG Key ID: 7E6F59EE25CDC6A5
43 changed files with 803 additions and 688 deletions

View File

@ -1,9 +1,5 @@
-keep class com.philj56.gbcc.SettingsFragment {
*;
}
-keep class com.philj56.gbcc.RomConfigFragment {
*;
<init>();
}
#-dontobfuscate

View File

@ -11,18 +11,14 @@
package com.philj56.gbcc
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.os.*
import android.util.AttributeSet
import android.view.*
import android.view.View.OnLongClickListener
import android.view.View.OnTouchListener
import android.widget.FrameLayout
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.WindowInsets
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.core.view.ViewCompat
@ -32,10 +28,8 @@ import androidx.preference.PreferenceManager
import com.google.android.material.slider.Slider
import com.philj56.gbcc.databinding.ActivityArrangeBinding
import kotlin.math.log
import kotlin.math.min
import kotlin.math.pow
class ArrangeActivity : BaseActivity() {
private val scaleFactorRange : Float = 2f
@ -311,135 +305,3 @@ class ArrangeActivity : BaseActivity() {
}
}
class ResizableImage : AppCompatImageView {
private var floating: Boolean = false
constructor(context: Context) : super(context) {
addMotionListener()
addLongClickListener()
}
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
addMotionListener()
addLongClickListener()
}
private fun addMotionListener() {
setOnTouchListener(OnTouchListener { view, motionEvent ->
if (!floating) {
return@OnTouchListener view.performClick()
}
when (motionEvent.actionMasked) {
MotionEvent.ACTION_UP -> {
floating = false
}
}
view.x = motionEvent.rawX - view.width / 2
view.y = motionEvent.rawY - view.height / 2
return@OnTouchListener true
})
}
private fun addLongClickListener() {
setOnLongClickListener(OnLongClickListener {
floating = true
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
return@OnLongClickListener false
})
}
}
class ResizableLayout : FrameLayout {
private var floating: Boolean = false
constructor(context: Context) : super(context) {
addMotionListener()
addLongClickListener()
}
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
addMotionListener()
addLongClickListener()
}
private fun addMotionListener() {
setOnTouchListener(OnTouchListener { view, motionEvent ->
if (!floating) {
return@OnTouchListener view.performClick()
}
when (motionEvent.actionMasked) {
MotionEvent.ACTION_UP -> {
floating = false
}
}
view.x = motionEvent.rawX - view.width / 2
view.y = motionEvent.rawY - view.height / 2
return@OnTouchListener true
})
}
private fun addLongClickListener() {
setOnLongClickListener(OnLongClickListener {
floating = true
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
return@OnLongClickListener false
})
}
}
class ScreenPlaceholder : AppCompatImageView {
constructor(context: Context) : super(context) {
setMeasuredDimension(160, 144)
layoutParams = ViewGroup.LayoutParams(160, 144)
}
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
setMeasuredDimension(160, 144)
layoutParams = ViewGroup.LayoutParams(160, 144)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
var width = 0
var height = 0
val scaleX = widthSize / 160
val scaleY = heightSize / 144
when(widthMode) {
MeasureSpec.EXACTLY -> width = widthSize
MeasureSpec.AT_MOST -> Unit
MeasureSpec.UNSPECIFIED -> width = 160
}
when(heightMode) {
MeasureSpec.EXACTLY -> height = heightSize
MeasureSpec.AT_MOST -> Unit
MeasureSpec.UNSPECIFIED -> height = 144
}
if (width == 0 && height == 0) {
val scale = min(scaleX, scaleY)
width = 160 * scale
height = 144 * scale
} else if (width == 0) {
width = (height * 160) / 144
} else if (height == 0) {
height = (width * 144) / 160
}
setMeasuredDimension(width, height)
}
}

View File

@ -10,26 +10,13 @@
package com.philj56.gbcc
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
import android.text.InputFilter
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.widget.SwitchCompat
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.philj56.gbcc.cheat.Cheat
import com.philj56.gbcc.cheat.CheatAdapter
import com.philj56.gbcc.cheat.CheatDialogFragment
import com.philj56.gbcc.databinding.ActivityCheatListBinding
import java.io.File
import kotlin.collections.ArrayList
data class Cheat(var description: String, var code: String, var active: Boolean)
class CheatActivity : BaseActivity() {
private lateinit var configFile: File
@ -133,103 +120,3 @@ class CheatActivity : BaseActivity() {
}
}
class CheatAdapter(
context: Context,
resource: Int,
textViewResourceId: Int,
private val objects: List<Cheat>
) : ArrayAdapter<Cheat>(context, resource, textViewResourceId, objects) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getView(position, convertView, parent)
val textDescription = view.findViewById<TextView>(R.id.cheatDescription)
val textCode = view.findViewById<TextView>(R.id.cheatCode)
val switchActive = view.findViewById<SwitchCompat>(R.id.cheatActive)
val (description, code, active) = objects[position]
textDescription.text = description
textCode.text = formatCode(code)
switchActive.isChecked = active
return view
}
private fun formatCode(code: String): String {
if (code.length == 9) {
return code.substring(0, 3) + "-" + code.substring(3, 6) + "-" + code.substring(6, 9)
}
return code
}
}
class CheatDialogFragment(private val index: Int) : DialogFragment() {
private var descriptionFilled = false
private var codeFilled = false
@SuppressLint("InflateParams")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val activity = requireActivity() as CheatActivity
val view = activity.layoutInflater.inflate(R.layout.dialog_edit_cheat, null, false)
val descriptionInput = view.findViewById<EditText>(R.id.cheatDescriptionInput)
val codeInput = view.findViewById<EditText>(R.id.cheatCodeInput)
val cheatExists = (index >= 0)
val title: Int
if (!cheatExists) {
title = R.string.cheat_add_description
} else {
title = R.string.cheat_edit_description
val cheat = activity.getCheat(index)
descriptionInput.setText(cheat.description)
codeInput.setText(cheat.code)
descriptionFilled = true
codeFilled = true
}
val builder = MaterialAlertDialogBuilder(activity)
.setTitle(title)
.setPositiveButton(android.R.string.ok) { _, _ ->
activity.addCheat(
Cheat(
descriptionInput.text.toString().trim(),
codeInput.text.toString().uppercase(),
true),
index
)
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.setView(view)
if (cheatExists) {
builder.setNeutralButton(R.string.delete) { _, _ -> activity.removeCheat(index) }
}
val dialog = builder.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = !cheatExists
}
descriptionInput.filters = arrayOf(InputFilter { source, start, end, _, _, _ ->
if (end <= start) {
// Deletion, always accept
return@InputFilter null
}
return@InputFilter source.subSequence(start, end).filterNot { it == '#' || it == ';' }
})
descriptionInput.addTextChangedListener { editor ->
descriptionFilled = editor?.isNotBlank() ?: false
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled =
descriptionFilled && codeFilled
}
codeInput.addTextChangedListener { editor ->
codeFilled = editor?.length == 8 || editor?.length == 9
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled =
descriptionFilled && codeFilled
}
return dialog
}
}

View File

@ -10,59 +10,47 @@
package com.philj56.gbcc
import android.animation.Animator
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.Dialog
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.OpenableColumns
import android.util.Log
import android.view.*
import android.view.MenuItem
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.inputmethod.InputMethodManager
import android.widget.*
import android.widget.Button
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.animation.addListener
import androidx.core.view.forEach
import androidx.core.view.postDelayed
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.DialogFragment
import androidx.preference.PreferenceManager
import androidx.transition.*
import androidx.transition.TransitionManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.philj56.gbcc.animations.CircularReveal
import com.philj56.gbcc.databinding.ActivityMainBinding
import com.philj56.gbcc.fileList.FileAdapter
import com.philj56.gbcc.fileList.PathAdapter
import com.philj56.gbcc.databinding.DialogDirectoryActionsBinding
import com.philj56.gbcc.databinding.DialogRomActionsBinding
import com.philj56.gbcc.main.*
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.lang.IllegalArgumentException
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipException
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
import kotlin.collections.ArrayList
import kotlin.collections.HashSet
import kotlin.math.max
private const val BACK_DELAY: Int = 2000
private const val SAVE_DIR: String = "saves"
private const val IMPORTED_SAVE_SUBDIR: String = "imported"
const val IMPORTED_SAVE_SUBDIR: String = "imported"
class MainActivity : BaseActivity() {
enum class SelectionMode {
private enum class SelectionMode {
NORMAL, MOVE, DELETE, SELECT
}
@ -373,54 +361,56 @@ class MainActivity : BaseActivity() {
}
private fun showDirectoryActionsDialog(file: File) {
val binding = DialogDirectoryActionsBinding.inflate(layoutInflater)
val dialog = MaterialAlertDialogBuilder(this)
.setView(R.layout.dialog_directory_actions)
.setView(binding.root)
.setOnCancelListener { clearSelection() }
.show()
dialog.findViewById<Button>(R.id.buttonSelectItems)?.setOnClickListener {
binding.buttonSelectItems.setOnClickListener {
beginSelection()
dialog.dismiss()
}
dialog.findViewById<Button>(R.id.buttonRenameDirectory)?.setOnClickListener {
binding.buttonRenameDirectory.setOnClickListener {
showRenameDialog(file)
dialog.dismiss()
}
}
private fun showRomActionsDialog(file: File) {
val binding = DialogRomActionsBinding.inflate(layoutInflater)
val dialog = MaterialAlertDialogBuilder(this)
.setView(R.layout.dialog_rom_actions)
.setView(binding.root)
.setOnCancelListener { clearSelection() }
.show()
dialog.findViewById<Button>(R.id.buttonMultiplayer)?.setOnClickListener {
binding.buttonMultiplayer.setOnClickListener {
MaterialAlertDialogBuilder(this)
.setView(R.layout.dialog_multiplayer_searching)
.setOnDismissListener { clearSelection() }
.show()
dialog.dismiss()
}
dialog.findViewById<Button>(R.id.buttonSelectItems)?.setOnClickListener {
binding.buttonSelectItems.setOnClickListener {
beginSelection()
dialog.dismiss()
}
dialog.findViewById<Button>(R.id.buttonRenameRom)?.setOnClickListener {
binding.buttonRenameRom.setOnClickListener {
showRenameDialog(file)
dialog.dismiss()
}
dialog.findViewById<Button>(R.id.buttonDeleteSave)?.setOnClickListener {
binding.buttonDeleteSave.setOnClickListener {
showSaveDeleteDialog()
dialog.dismiss()
}
dialog.findViewById<Button>(R.id.buttonEditCheats)?.setOnClickListener {
binding.buttonEditCheats.setOnClickListener {
val intent = Intent(this, CheatActivity::class.java).apply {
putExtra("file", file.name)
}
startActivity(intent)
dialog.dismiss()
}
dialog.findViewById<Button>(R.id.buttonEditConfig)?.setOnClickListener {
binding.buttonEditConfig.setOnClickListener {
val intent = Intent(this, RomConfigActivity::class.java).apply {
putExtra("file", file.name)
}
@ -745,182 +735,3 @@ class MainActivity : BaseActivity() {
.show()
}
}
class ImportOverwriteAdapter(
context: Context,
resource: Int,
textViewResourceId: Int,
private val objects: List<File>
) : ArrayAdapter<File>(context, resource, textViewResourceId, objects) {
val selected = HashSet<File>()
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view: View = super.getView(position, convertView, parent)
val textView: CheckedTextView = view.findViewById(R.id.importOverwriteText)
val file: File = objects[position]
textView.isChecked = file in selected
textView.text = file.nameWithoutExtension
return view
}
}
class EditTextDialogFragment(private val title: Int, private val initialText: String, private val onConfirm: (String) -> Unit) : DialogFragment() {
private var onDismissListener: (() -> Unit)? = null
@SuppressLint("InflateParams")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val textView = it.layoutInflater.inflate(R.layout.dialog_create_folder, null, false)
val input = textView?.findViewById<EditText>(R.id.createFolderInput)
input?.setText(initialText)
val builder = MaterialAlertDialogBuilder(it)
builder.setTitle(title)
.setPositiveButton(android.R.string.ok) { _, _ ->
onConfirm(input?.text.toString())
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.setView(textView)
val dialog = builder.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
}
input?.addTextChangedListener { editor ->
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled =
editor?.isNotBlank() ?: false
}
input?.setOnFocusChangeListener { v, hasFocus ->
v.postDelayed(50) {
if (hasFocus) {
val imm =
context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(v, 0)
}
}
}
input?.requestFocus()
return dialog
} ?: throw IllegalStateException("Activity cannot be null")
}
override fun onDismiss(dialog: DialogInterface) {
onDismissListener?.invoke()
super.onDismiss(dialog)
}
fun setOnDismissListener(listener: () -> Unit) {
onDismissListener = listener
}
}
class ImportOverwriteDialogFragment(private val files: ArrayList<File>) : DialogFragment() {
@SuppressLint("InflateParams")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val adapter = ImportOverwriteAdapter(requireContext(), R.layout.entry_import_overwrite, R.id.importOverwriteText, files)
adapter.selected.addAll(files)
val view = it.layoutInflater.inflate(R.layout.dialog_import_overwrite, null, false)
val listView = view.findViewById<ListView>(R.id.listView)
listView.adapter = adapter
listView.setOnItemClickListener { _, _, position, _ ->
val file = listView.adapter.getItem(position) as File
val item = listView.getChildAt(position - listView.firstVisiblePosition)
if (file in adapter.selected) {
adapter.selected.remove(file)
} else {
adapter.selected.add(file)
}
item.findViewById<CheckedTextView>(R.id.importOverwriteText).isChecked = file in adapter.selected
}
val saveDir = requireContext().filesDir.resolve("saves")
fun deleteFiles() {
saveDir.resolve(IMPORTED_SAVE_SUBDIR).deleteRecursively()
}
val builder = MaterialAlertDialogBuilder(it)
builder.setTitle(resources.getString(R.string.overwrite_confirmation))
.setPositiveButton(android.R.string.ok) { _, _ ->
Thread {
adapter.selected.forEach { file ->
val dest = saveDir.resolve(file.name)
dest.delete()
file.renameTo(dest)
}
activity?.runOnUiThread {
Toast.makeText(
context,
resources.getQuantityString(
R.plurals.message_imported,
adapter.selected.size,
adapter.selected.size
),
Toast.LENGTH_SHORT
).show()
}
deleteFiles()
}.start()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> deleteFiles() }
.setView(view)
.setOnDismissListener { deleteFiles() }
return builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
}
class CreateSaveExportZip : ActivityResultContracts.CreateDocument() {
@SuppressLint("SimpleDateFormat")
override fun createIntent(context: Context, input: String): Intent {
val intent = super.createIntent(context, input)
intent.apply {
addCategory(Intent.CATEGORY_OPENABLE)
}
val date = SimpleDateFormat("yyyyMMdd").format(Date())
intent.putExtra(Intent.EXTRA_TITLE, "gbcc_saves_$date.zip")
return intent
}
}
class CircularReveal : Visibility() {
override fun onAppear(
sceneRoot: ViewGroup,
view: View,
startValues: TransitionValues,
endValues: TransitionValues
): Animator {
val animator = ViewAnimationUtils.createCircularReveal(
view,
view.width / 2,
view.height / 2,
0.0f,
max(view.width, view.height).toFloat()
)
view.alpha = 0.0f
animator.addListener(
onStart = { view.alpha = 1.0f }
)
return animator
}
override fun onDisappear(
sceneRoot: ViewGroup,
view: View,
startValues: TransitionValues,
endValues: TransitionValues
): Animator {
return ViewAnimationUtils.createCircularReveal(
view,
view.width / 2,
view.height / 2,
max(view.width, view.height).toFloat(),
0.0f
)
}
}

View File

@ -1,20 +1,17 @@
package com.philj56.gbcc
import android.app.Dialog
import android.content.SharedPreferences
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.os.Bundle
import android.util.TypedValue
import android.view.*
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import android.widget.Button
import android.widget.RadioGroup
import androidx.core.content.edit
import androidx.fragment.app.DialogFragment
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.radiobutton.MaterialRadioButton
import com.philj56.gbcc.databinding.ActivityRemapControllerBinding
import com.philj56.gbcc.remap.RemapButtonDialogFragment
import kotlin.math.abs
class RemapActivity : BaseActivity() {
@ -194,46 +191,3 @@ class RemapActivity : BaseActivity() {
}
}
class RemapButtonDialogFragment(private val button: Button, private val key: String, private val analogue: Boolean) :
DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
val mapping = prefs.getString(key, "unmapped")
val buttonNames: Array<String>
val buttonValues: Array<String>
if (analogue) {
buttonNames = resources.getStringArray(R.array.button_map_analogue_names_array)
buttonValues = resources.getStringArray(R.array.button_map_analogue_values_array)
} else {
buttonNames = resources.getStringArray(R.array.button_map_names_array)
buttonValues = resources.getStringArray(R.array.button_map_values_array)
}
val layout = requireActivity().layoutInflater.inflate(R.layout.dialog_remap_button, null, false)
val radioGroup = layout.findViewById<RadioGroup>(R.id.remapDialogRadioGroup)
buttonNames.forEachIndexed { index, name ->
val radio = MaterialRadioButton(radioGroup.context)
radio.text = name
radio.id = index
radioGroup.addView(radio)
if (mapping == buttonValues[index]) {
radioGroup.check(index)
}
}
val builder = MaterialAlertDialogBuilder(requireActivity())
builder.setTitle(R.string.select_mapping)
.setPositiveButton(android.R.string.ok) { _, _ ->
prefs.edit {
putString(key, buttonValues[radioGroup.checkedRadioButtonId])
apply()
}
button.text = buttonNames[radioGroup.checkedRadioButtonId]
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.setView(layout)
return builder.create()
}
}

View File

@ -11,8 +11,6 @@
package com.philj56.gbcc
import android.os.Bundle
import androidx.preference.*
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.philj56.gbcc.databinding.ActivityRomConfigBinding
import java.io.File
@ -41,36 +39,3 @@ class RomConfigActivity : BaseActivity() {
}
}
class RomConfigFragment : PreferenceFragmentCompat() {
private lateinit var dataStore: IniDataStore
private var save = true
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
dataStore = IniDataStore((requireActivity() as RomConfigActivity).configFile)
preferenceManager.preferenceDataStore = dataStore
setPreferencesFromResource(R.xml.rom_config, rootKey)
}
override fun onDestroy() {
if (save) {
dataStore.saveFile()
}
super.onDestroy()
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
if (preference.key == "delete") {
val context = requireContext()
MaterialAlertDialogBuilder(context)
.setTitle(R.string.delete_config_confirmation)
.setPositiveButton(R.string.delete) { _, _ ->
save = false
(activity as RomConfigActivity).clearConfig()
}.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
return true
}
return super.onPreferenceTreeClick(preference)
}
}

View File

@ -10,21 +10,17 @@
package com.philj56.gbcc
import android.content.Context
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.AttributeSet
import android.widget.TextView
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.bundleOf
import androidx.fragment.app.add
import androidx.fragment.app.commit
import androidx.preference.*
import com.philj56.gbcc.databinding.ActivitySettingsBinding
import com.philj56.gbcc.preference.MaterialTurboPreferenceDialogFragmentCompat
import com.philj56.gbcc.preference.MaterialListPreferenceDialogFragmentCompat
import com.philj56.gbcc.preference.SliderPreference
import com.philj56.gbcc.materialPreferences.MaterialListPreferenceDialogFragmentCompat
import com.philj56.gbcc.materialPreferences.MaterialTurboPreferenceDialogFragmentCompat
import com.philj56.gbcc.settings.SummaryListPreference
import com.philj56.gbcc.settings.TurboPreference
abstract class BaseSettingsActivity : BaseActivity() {
abstract val preferenceResource : Int
@ -120,72 +116,3 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
}
class SummaryListPreference(context: Context, attrs: AttributeSet) :
ListPreference(context, attrs) {
init {
summaryProvider = SimpleSummaryProvider.getInstance()
}
}
class TurboPreference(context: Context, attrs: AttributeSet) :
EditTextPreference(context, attrs) {
init {
summaryProvider = TurboSummaryProvider
// This is currently unneeded, as it's hardcoded into
// MaterialTurboPreferenceDialogFragmentCompat.
// If that ever gets changed back to a less hacky solution, we'll want this again.
// setOnBindEditTextListener { editText ->
// editText.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
// editText.selectAll()
// }
}
override fun setText(text: String?) {
if (text?.toFloatOrNull() == null) {
super.setText("0")
} else {
super.setText(text)
}
}
}
object TurboSummaryProvider : Preference.SummaryProvider<EditTextPreference> {
override fun provideSummary(preference: EditTextPreference): CharSequence {
val text = preference.text?.ifEmpty {
"0"
}
return when (text?.toFloat()) {
0F -> "0 (Unlimited)"
else -> "$text×"
}
}
}
class UnitSeekbarPreference(context: Context, attrs: AttributeSet) :
SliderPreference(context, attrs) {
lateinit var textView: TextView
val watcher = UnitSeekbarPreferenceWatcher()
override fun onBindViewHolder(view: PreferenceViewHolder) {
textView = view.findViewById(R.id.seekbar_value) as TextView
textView.removeTextChangedListener(watcher)
textView.addTextChangedListener(watcher)
super.onBindViewHolder(view)
}
inner class UnitSeekbarPreferenceWatcher : TextWatcher {
override fun afterTextChanged(s: Editable?) {
textView.removeTextChangedListener(watcher)
s?.insert(s.length, context.resources.getString(R.string.settings_bluetooth_latency_units))
textView.addTextChangedListener(watcher)
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
}
}

View File

@ -0,0 +1,47 @@
package com.philj56.gbcc.animations
import android.animation.Animator
import android.view.View
import android.view.ViewAnimationUtils
import android.view.ViewGroup
import androidx.core.animation.addListener
import androidx.transition.TransitionValues
import androidx.transition.Visibility
import kotlin.math.max
class CircularReveal : Visibility() {
override fun onAppear(
sceneRoot: ViewGroup,
view: View,
startValues: TransitionValues,
endValues: TransitionValues
): Animator {
val animator = ViewAnimationUtils.createCircularReveal(
view,
view.width / 2,
view.height / 2,
0.0f,
max(view.width, view.height).toFloat()
)
view.alpha = 0.0f
animator.addListener(
onStart = { view.alpha = 1.0f }
)
return animator
}
override fun onDisappear(
sceneRoot: ViewGroup,
view: View,
startValues: TransitionValues,
endValues: TransitionValues
): Animator {
return ViewAnimationUtils.createCircularReveal(
view,
view.width / 2,
view.height / 2,
max(view.width, view.height).toFloat(),
0.0f
)
}
}

View File

@ -0,0 +1,50 @@
package com.philj56.gbcc.arrange
import android.content.Context
import android.util.AttributeSet
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import androidx.appcompat.widget.AppCompatImageView
class ResizableImage : AppCompatImageView {
private var floating: Boolean = false
constructor(context: Context) : super(context) {
addMotionListener()
addLongClickListener()
}
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
addMotionListener()
addLongClickListener()
}
private fun addMotionListener() {
setOnTouchListener(OnTouchListener { view, motionEvent ->
if (!floating) {
return@OnTouchListener view.performClick()
}
when (motionEvent.actionMasked) {
MotionEvent.ACTION_UP -> {
floating = false
}
}
view.x = motionEvent.rawX - view.width / 2
view.y = motionEvent.rawY - view.height / 2
return@OnTouchListener true
})
}
private fun addLongClickListener() {
setOnLongClickListener(OnLongClickListener {
floating = true
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
return@OnLongClickListener false
})
}
}

View File

@ -0,0 +1,49 @@
package com.philj56.gbcc.arrange
import android.content.Context
import android.util.AttributeSet
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.widget.FrameLayout
class ResizableLayout : FrameLayout {
private var floating: Boolean = false
constructor(context: Context) : super(context) {
addMotionListener()
addLongClickListener()
}
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
addMotionListener()
addLongClickListener()
}
private fun addMotionListener() {
setOnTouchListener(OnTouchListener { view, motionEvent ->
if (!floating) {
return@OnTouchListener view.performClick()
}
when (motionEvent.actionMasked) {
MotionEvent.ACTION_UP -> {
floating = false
}
}
view.x = motionEvent.rawX - view.width / 2
view.y = motionEvent.rawY - view.height / 2
return@OnTouchListener true
})
}
private fun addLongClickListener() {
setOnLongClickListener(OnLongClickListener {
floating = true
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
return@OnLongClickListener false
})
}
}

View File

@ -0,0 +1,56 @@
package com.philj56.gbcc.arrange
import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
import androidx.appcompat.widget.AppCompatImageView
import kotlin.math.min
class ScreenPlaceholder : AppCompatImageView {
constructor(context: Context) : super(context) {
setMeasuredDimension(160, 144)
layoutParams = ViewGroup.LayoutParams(160, 144)
}
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
setMeasuredDimension(160, 144)
layoutParams = ViewGroup.LayoutParams(160, 144)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
var width = 0
var height = 0
val scaleX = widthSize / 160
val scaleY = heightSize / 144
when(widthMode) {
MeasureSpec.EXACTLY -> width = widthSize
MeasureSpec.AT_MOST -> Unit
MeasureSpec.UNSPECIFIED -> width = 160
}
when(heightMode) {
MeasureSpec.EXACTLY -> height = heightSize
MeasureSpec.AT_MOST -> Unit
MeasureSpec.UNSPECIFIED -> height = 144
}
if (width == 0 && height == 0) {
val scale = min(scaleX, scaleY)
width = 160 * scale
height = 144 * scale
} else if (width == 0) {
width = (height * 160) / 144
} else if (height == 0) {
height = (width * 144) / 160
}
setMeasuredDimension(width, height)
}
}

View File

@ -0,0 +1,3 @@
package com.philj56.gbcc.cheat
data class Cheat(var description: String, var code: String, var active: Boolean)

View File

@ -0,0 +1,38 @@
package com.philj56.gbcc.cheat
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import androidx.appcompat.widget.SwitchCompat
import com.philj56.gbcc.R
class CheatAdapter(
context: Context,
resource: Int,
textViewResourceId: Int,
private val objects: List<Cheat>
) : ArrayAdapter<Cheat>(context, resource, textViewResourceId, objects) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getView(position, convertView, parent)
val textDescription = view.findViewById<TextView>(R.id.cheatDescription)
val textCode = view.findViewById<TextView>(R.id.cheatCode)
val switchActive = view.findViewById<SwitchCompat>(R.id.cheatActive)
val (description, code, active) = objects[position]
textDescription.text = description
textCode.text = formatCode(code)
switchActive.isChecked = active
return view
}
private fun formatCode(code: String): String {
if (code.length == 9) {
return code.substring(0, 3) + "-" + code.substring(3, 6) + "-" + code.substring(6, 9)
}
return code
}
}

View File

@ -0,0 +1,85 @@
package com.philj56.gbcc.cheat
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.Dialog
import android.os.Bundle
import android.text.InputFilter
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.philj56.gbcc.CheatActivity
import com.philj56.gbcc.R
import com.philj56.gbcc.databinding.DialogEditCheatBinding
class CheatDialogFragment(private val index: Int) : DialogFragment() {
private var descriptionFilled = false
private var codeFilled = false
@SuppressLint("InflateParams")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val activity = requireActivity() as CheatActivity
val view = activity.layoutInflater.inflate(R.layout.dialog_edit_cheat, null, false)
val binding = DialogEditCheatBinding.bind(view)
val cheatExists = (index >= 0)
val title: Int
if (!cheatExists) {
title = R.string.cheat_add_description
} else {
title = R.string.cheat_edit_description
val cheat = activity.getCheat(index)
binding.cheatDescriptionInput.setText(cheat.description)
binding.cheatCodeInput.setText(cheat.code)
descriptionFilled = true
codeFilled = true
}
val builder = MaterialAlertDialogBuilder(activity)
.setTitle(title)
.setPositiveButton(android.R.string.ok) { _, _ ->
activity.addCheat(
Cheat(
binding.cheatDescriptionInput.text.toString().trim(),
binding.cheatCodeInput.text.toString().uppercase(),
true
),
index
)
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.setView(view)
if (cheatExists) {
builder.setNeutralButton(R.string.delete) { _, _ -> activity.removeCheat(index) }
}
val dialog = builder.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
}
binding.cheatDescriptionInput.filters = arrayOf(InputFilter { source, start, end, _, _, _ ->
if (end <= start) {
// Deletion, always accept
return@InputFilter null
}
return@InputFilter source.subSequence(start, end).filterNot { it == '#' || it == ';' }
})
binding.cheatDescriptionInput.addTextChangedListener { editor ->
descriptionFilled = editor?.isNotBlank() ?: false
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled =
descriptionFilled && codeFilled
}
binding.cheatCodeInput.addTextChangedListener { editor ->
codeFilled = editor?.length == 8 || editor?.length == 9
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled =
descriptionFilled && codeFilled
}
return dialog
}
}

View File

@ -0,0 +1,21 @@
package com.philj56.gbcc.main
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContracts
import java.text.SimpleDateFormat
import java.util.*
class CreateSaveExportZip : ActivityResultContracts.CreateDocument() {
@SuppressLint("SimpleDateFormat")
override fun createIntent(context: Context, input: String): Intent {
val intent = super.createIntent(context, input)
intent.apply {
addCategory(Intent.CATEGORY_OPENABLE)
}
val date = SimpleDateFormat("yyyyMMdd").format(Date())
intent.putExtra(Intent.EXTRA_TITLE, "gbcc_saves_$date.zip")
return intent
}
}

View File

@ -0,0 +1,71 @@
package com.philj56.gbcc.main
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import androidx.core.view.postDelayed
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.philj56.gbcc.R
class EditTextDialogFragment(
private val title: Int,
private val initialText: String,
private val onConfirm: (String) -> Unit
) : DialogFragment() {
private var onDismissListener: (() -> Unit)? = null
@SuppressLint("InflateParams")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val textView = it.layoutInflater.inflate(R.layout.dialog_create_folder, null, false)
val input = textView?.findViewById<EditText>(R.id.createFolderInput)
input?.setText(initialText)
val builder = MaterialAlertDialogBuilder(it)
builder.setTitle(title)
.setPositiveButton(android.R.string.ok) { _, _ ->
onConfirm(input?.text.toString())
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.setView(textView)
val dialog = builder.create()
dialog.setOnShowListener {
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
}
input?.addTextChangedListener { editor ->
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled =
editor?.isNotBlank() ?: false
}
input?.setOnFocusChangeListener { v, hasFocus ->
v.postDelayed(50) {
if (hasFocus) {
val imm =
context?.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(v, 0)
}
}
}
input?.requestFocus()
return dialog
} ?: throw IllegalStateException("Activity cannot be null")
}
override fun onDismiss(dialog: DialogInterface) {
onDismissListener?.invoke()
super.onDismiss(dialog)
}
fun setOnDismissListener(listener: () -> Unit) {
onDismissListener = listener
}
}

View File

@ -1,4 +1,4 @@
package com.philj56.gbcc.fileList
package com.philj56.gbcc.main
import android.view.LayoutInflater
import android.view.View
@ -11,7 +11,10 @@ import androidx.recyclerview.widget.RecyclerView
import com.philj56.gbcc.R
import java.io.File
class FileAdapter(private val onClick: (File, View) -> Unit, private val onLongClick: (File, View) -> Unit) :
class FileAdapter(
private val onClick: (File, View) -> Unit,
private val onLongClick: (File, View) -> Unit
) :
ListAdapter<File, FileAdapter.FileViewHolder>(FileDiffCallback) {
val selected = HashSet<File>()

View File

@ -0,0 +1,31 @@
package com.philj56.gbcc.main
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.CheckedTextView
import com.philj56.gbcc.R
import java.io.File
import java.util.HashSet
class ImportOverwriteAdapter(
context: Context,
resource: Int,
textViewResourceId: Int,
private val objects: List<File>
) : ArrayAdapter<File>(context, resource, textViewResourceId, objects) {
val selected = HashSet<File>()
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view: View = super.getView(position, convertView, parent)
val textView: CheckedTextView = view.findViewById(R.id.importOverwriteText)
val file: File = objects[position]
textView.isChecked = file in selected
textView.text = file.nameWithoutExtension
return view
}
}

View File

@ -0,0 +1,73 @@
package com.philj56.gbcc.main
import android.annotation.SuppressLint
import android.app.Dialog
import android.os.Bundle
import android.widget.CheckedTextView
import android.widget.ListView
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.philj56.gbcc.IMPORTED_SAVE_SUBDIR
import com.philj56.gbcc.R
import java.io.File
class ImportOverwriteDialogFragment(private val files: ArrayList<File>) : DialogFragment() {
@SuppressLint("InflateParams")
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return activity?.let {
val adapter = ImportOverwriteAdapter(
requireContext(),
R.layout.entry_import_overwrite,
R.id.importOverwriteText,
files
)
adapter.selected.addAll(files)
val view = it.layoutInflater.inflate(R.layout.dialog_import_overwrite, null, false)
val listView = view.findViewById<ListView>(R.id.listView)
listView.adapter = adapter
listView.setOnItemClickListener { _, _, position, _ ->
val file = listView.adapter.getItem(position) as File
val item = listView.getChildAt(position - listView.firstVisiblePosition)
if (file in adapter.selected) {
adapter.selected.remove(file)
} else {
adapter.selected.add(file)
}
item.findViewById<CheckedTextView>(R.id.importOverwriteText).isChecked = file in adapter.selected
}
val saveDir = requireContext().filesDir.resolve("saves")
fun deleteFiles() {
saveDir.resolve(IMPORTED_SAVE_SUBDIR).deleteRecursively()
}
val builder = MaterialAlertDialogBuilder(it)
builder.setTitle(resources.getString(R.string.overwrite_confirmation))
.setPositiveButton(android.R.string.ok) { _, _ ->
Thread {
adapter.selected.forEach { file ->
val dest = saveDir.resolve(file.name)
dest.delete()
file.renameTo(dest)
}
activity?.runOnUiThread {
Toast.makeText(
context,
resources.getQuantityString(
R.plurals.message_imported,
adapter.selected.size,
adapter.selected.size
),
Toast.LENGTH_SHORT
).show()
}
deleteFiles()
}.start()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> deleteFiles() }
.setView(view)
.setOnDismissListener { deleteFiles() }
return builder.create()
} ?: throw IllegalStateException("Activity cannot be null")
}
}

View File

@ -1,4 +1,4 @@
package com.philj56.gbcc.fileList
package com.philj56.gbcc.main
import android.annotation.SuppressLint
import android.view.LayoutInflater

View File

@ -19,7 +19,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.philj56.gbcc.preference;
package com.philj56.gbcc.materialPreferences;
import android.content.DialogInterface;
import android.os.Bundle;

View File

@ -20,7 +20,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.philj56.gbcc.preference;
package com.philj56.gbcc.materialPreferences;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import android.app.Dialog;

View File

@ -20,7 +20,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.philj56.gbcc.preference;
package com.philj56.gbcc.materialPreferences;
import android.content.Context;
import android.content.res.TypedArray;
@ -57,7 +57,7 @@ import com.philj56.gbcc.R;
* max})
* can be set directly on the preference widget layout.
*/
public class SliderPreference extends Preference {
public class MaterialSeekbarPreference extends Preference {
private static final String TAG = "SliderPreference";
@SuppressWarnings("WeakerAccess") /* synthetic access */
int mSeekBarValue;
@ -140,7 +140,7 @@ public class SliderPreference extends Preference {
return mSeekBar.onKeyDown(keyCode, event);
}
};
public SliderPreference(
public MaterialSeekbarPreference(
Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
TypedArray a = context.obtainStyledAttributes(
@ -156,13 +156,13 @@ public class SliderPreference extends Preference {
mUpdatesContinuously = false; //a.getBoolean(R.styleable.SeekBarPreference_updatesContinuously, false);
a.recycle();
}
public SliderPreference(Context context, AttributeSet attrs, int defStyleAttr) {
public MaterialSeekbarPreference(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public SliderPreference(Context context, AttributeSet attrs) {
public MaterialSeekbarPreference(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.seekBarPreferenceStyle);
}
public SliderPreference(Context context) {
public MaterialSeekbarPreference(Context context) {
this(context, null);
}
@Override

View File

@ -21,7 +21,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.philj56.gbcc.preference;
package com.philj56.gbcc.materialPreferences;
import android.os.Bundle;
import android.text.InputType;
import android.view.View;

View File

@ -18,7 +18,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.philj56.gbcc.preference;
package com.philj56.gbcc.materialPreferences;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.LinearLayout;

View File

@ -0,0 +1,56 @@
package com.philj56.gbcc.remap
import android.app.Dialog
import android.os.Bundle
import android.widget.Button
import android.widget.RadioGroup
import androidx.core.content.edit
import androidx.fragment.app.DialogFragment
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.radiobutton.MaterialRadioButton
import com.philj56.gbcc.R
class RemapButtonDialogFragment(private val button: Button, private val key: String, private val analogue: Boolean) :
DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
val mapping = prefs.getString(key, "unmapped")
val buttonNames: Array<String>
val buttonValues: Array<String>
if (analogue) {
buttonNames = resources.getStringArray(R.array.button_map_analogue_names_array)
buttonValues = resources.getStringArray(R.array.button_map_analogue_values_array)
} else {
buttonNames = resources.getStringArray(R.array.button_map_names_array)
buttonValues = resources.getStringArray(R.array.button_map_values_array)
}
val layout = requireActivity().layoutInflater.inflate(R.layout.dialog_remap_button, null, false)
val radioGroup = layout.findViewById<RadioGroup>(R.id.remapDialogRadioGroup)
buttonNames.forEachIndexed { index, name ->
val radio = MaterialRadioButton(radioGroup.context)
radio.text = name
radio.id = index
radioGroup.addView(radio)
if (mapping == buttonValues[index]) {
radioGroup.check(index)
}
}
val builder = MaterialAlertDialogBuilder(requireActivity())
builder.setTitle(R.string.select_mapping)
.setPositiveButton(android.R.string.ok) { _, _ ->
prefs.edit {
putString(key, buttonValues[radioGroup.checkedRadioButtonId])
apply()
}
button.text = buttonNames[radioGroup.checkedRadioButtonId]
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.setView(layout)
return builder.create()
}
}

View File

@ -1,4 +1,4 @@
package com.philj56.gbcc
package com.philj56.gbcc.romConfig
import androidx.preference.PreferenceDataStore
import java.io.File

View File

@ -0,0 +1,41 @@
package com.philj56.gbcc.romConfig
import android.os.Bundle
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.philj56.gbcc.R
import com.philj56.gbcc.RomConfigActivity
class RomConfigFragment : PreferenceFragmentCompat() {
private lateinit var dataStore: IniDataStore
private var save = true
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
dataStore = IniDataStore((requireActivity() as RomConfigActivity).configFile)
preferenceManager.preferenceDataStore = dataStore
setPreferencesFromResource(R.xml.rom_config, rootKey)
}
override fun onDestroy() {
if (save) {
dataStore.saveFile()
}
super.onDestroy()
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
if (preference.key == "delete") {
val context = requireContext()
MaterialAlertDialogBuilder(context)
.setTitle(R.string.delete_config_confirmation)
.setPositiveButton(R.string.delete) { _, _ ->
save = false
(activity as RomConfigActivity).clearConfig()
}.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
return true
}
return super.onPreferenceTreeClick(preference)
}
}

View File

@ -0,0 +1,12 @@
package com.philj56.gbcc.settings
import android.content.Context
import android.util.AttributeSet
import androidx.preference.ListPreference
class SummaryListPreference(context: Context, attrs: AttributeSet) :
ListPreference(context, attrs) {
init {
summaryProvider = SimpleSummaryProvider.getInstance()
}
}

View File

@ -0,0 +1,41 @@
package com.philj56.gbcc.settings
import android.content.Context
import android.util.AttributeSet
import androidx.preference.EditTextPreference
class TurboPreference(context: Context, attrs: AttributeSet) :
EditTextPreference(context, attrs) {
init {
summaryProvider = TurboSummaryProvider
// This is currently unneeded, as it's hardcoded into
// MaterialTurboPreferenceDialogFragmentCompat.
// If that ever gets changed back to a less hacky solution, we'll want this again.
// setOnBindEditTextListener { editText ->
// editText.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL
// editText.selectAll()
// }
}
override fun setText(text: String?) {
if (text?.toFloatOrNull() == null) {
super.setText("0")
} else {
super.setText(text)
}
}
private object TurboSummaryProvider : SummaryProvider<EditTextPreference> {
override fun provideSummary(preference: EditTextPreference): CharSequence {
val text = preference.text?.ifEmpty {
"0"
}
return when (text?.toFloat()) {
0F -> "0 (Unlimited)"
else -> "$text×"
}
}
}
}

View File

@ -0,0 +1,38 @@
package com.philj56.gbcc.settings
import android.content.Context
import android.text.Editable
import android.text.TextWatcher
import android.util.AttributeSet
import android.widget.TextView
import androidx.preference.PreferenceViewHolder
import com.philj56.gbcc.R
import com.philj56.gbcc.materialPreferences.MaterialSeekbarPreference
class UnitSeekbarPreference(context: Context, attrs: AttributeSet) :
MaterialSeekbarPreference(context, attrs) {
lateinit var textView: TextView
val watcher = UnitSeekbarPreferenceWatcher()
override fun onBindViewHolder(view: PreferenceViewHolder) {
textView = view.findViewById(R.id.seekbar_value) as TextView
textView.removeTextChangedListener(watcher)
textView.addTextChangedListener(watcher)
super.onBindViewHolder(view)
}
inner class UnitSeekbarPreferenceWatcher : TextWatcher {
override fun afterTextChanged(s: Editable?) {
textView.removeTextChangedListener(watcher)
s?.insert(s.length, context.resources.getString(R.string.settings_bluetooth_latency_units))
textView.addTextChangedListener(watcher)
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
}
}

View File

@ -23,7 +23,7 @@
and ignores any further touches. -->
</ImageView>
<com.philj56.gbcc.ScreenPlaceholder
<com.philj56.gbcc.arrange.ScreenPlaceholder
android:id="@+id/screen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -75,7 +75,7 @@
app:layout_constraintEnd_toEndOf="@+id/screenBorderRight"
app:layout_constraintStart_toStartOf="@+id/screenBorderLeft" />
<com.philj56.gbcc.ResizableImage
<com.philj56.gbcc.arrange.ResizableImage
android:id="@+id/buttonA"
android:layout_width="64dp"
android:layout_height="64dp"
@ -93,7 +93,7 @@
app:layout_constraintStart_toEndOf="@+id/screenBorderRight"
app:layout_constraintTop_toTopOf="parent" />
<com.philj56.gbcc.ResizableImage
<com.philj56.gbcc.arrange.ResizableImage
android:id="@+id/buttonB"
android:layout_width="64dp"
android:layout_height="64dp"
@ -109,7 +109,7 @@
app:layout_constraintStart_toEndOf="@+id/screenBorderRight"
app:layout_constraintTop_toBottomOf="@+id/buttonA" />
<com.philj56.gbcc.ResizableImage
<com.philj56.gbcc.arrange.ResizableImage
android:id="@+id/buttonSelect"
android:layout_width="64dp"
android:layout_height="48dp"
@ -128,7 +128,7 @@
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintVertical_weight="0" />
<com.philj56.gbcc.ResizableImage
<com.philj56.gbcc.arrange.ResizableImage
android:id="@+id/buttonStart"
android:layout_width="64dp"
android:layout_height="48dp"
@ -216,7 +216,6 @@
<Button
android:id="@+id/resetSizes"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
@ -227,7 +226,6 @@
<Button
android:id="@+id/resetLayout"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"

View File

@ -23,7 +23,7 @@
and ignores any further touches. -->
</ImageView>
<com.philj56.gbcc.ScreenPlaceholder
<com.philj56.gbcc.arrange.ScreenPlaceholder
android:id="@+id/screen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -75,7 +75,7 @@
app:layout_constraintEnd_toEndOf="@+id/screenBorderRight"
app:layout_constraintStart_toStartOf="@+id/screenBorderLeft" />
<com.philj56.gbcc.ResizableImage
<com.philj56.gbcc.arrange.ResizableImage
android:id="@+id/buttonA"
android:layout_width="64dp"
android:layout_height="64dp"
@ -90,7 +90,7 @@
app:layout_constraintTop_toTopOf="@+id/dpad"
app:layout_constraintVertical_bias="0.33" />
<com.philj56.gbcc.ResizableImage
<com.philj56.gbcc.arrange.ResizableImage
android:id="@+id/buttonB"
android:layout_width="64dp"
android:layout_height="64dp"
@ -109,7 +109,7 @@
app:layout_constraintTop_toTopOf="@+id/buttonA"
app:layout_constraintVertical_bias="0.0" />
<com.philj56.gbcc.ResizableImage
<com.philj56.gbcc.arrange.ResizableImage
android:id="@+id/buttonStart"
android:layout_width="64dp"
android:layout_height="48dp"
@ -125,7 +125,7 @@
app:layout_constraintStart_toEndOf="@+id/buttonSelect"
app:layout_constraintTop_toTopOf="@+id/buttonSelect" />
<com.philj56.gbcc.ResizableImage
<com.philj56.gbcc.arrange.ResizableImage
android:id="@+id/buttonSelect"
android:layout_width="64dp"
android:layout_height="48dp"

View File

@ -38,7 +38,7 @@ the collapsing bar work properly when rotating / toggling night mode
<androidx.fragment.app.FragmentContainerView
android:id="@+id/romConfigFragment"
android:name="com.philj56.gbcc.RomConfigFragment"
android:name="com.philj56.gbcc.romConfig.RomConfigFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<com.philj56.gbcc.ResizableLayout xmlns:android="http://schemas.android.com/apk/res/android"
<com.philj56.gbcc.arrange.ResizableLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:contentDescription="@string/dpad_description"
android:layout_width="match_parent"
@ -19,4 +19,4 @@
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_button_dpad_highlight" />
</com.philj56.gbcc.ResizableLayout>
</com.philj56.gbcc.arrange.ResizableLayout>

View File

@ -78,7 +78,7 @@
to the children of this container layout. Otherwise, the animated pressed state will also
play for the thumb in the AbsSeekBar in addition to the preference's ripple background.
The background of the SeekBar is also set to null to disable the ripple background -->
<com.philj56.gbcc.preference.UnPressableLinearLayout
<com.philj56.gbcc.materialPreferences.UnPressableLinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
@ -120,6 +120,6 @@
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:scrollbars="none"/>
</com.philj56.gbcc.preference.UnPressableLinearLayout>
</com.philj56.gbcc.materialPreferences.UnPressableLinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<com.philj56.gbcc.ResizableLayout xmlns:android="http://schemas.android.com/apk/res/android"
<com.philj56.gbcc.arrange.ResizableLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -16,4 +16,4 @@
android:theme="@style/TurboToggleTheme"
app:switchMinWidth="64dp" />
</com.philj56.gbcc.ResizableLayout>
</com.philj56.gbcc.arrange.ResizableLayout>

View File

@ -17,7 +17,7 @@
app:title="@string/settings_audio_low_latency"
app:summary="@string/settings_audio_low_latency_summary" />
<com.philj56.gbcc.UnitSeekbarPreference
<com.philj56.gbcc.settings.UnitSeekbarPreference
android:layout="@layout/preference_widget_slider"
app:dependency="audio_low_latency"
app:min="40"
@ -29,7 +29,7 @@
app:summary="@string/settings_audio_latency_summary"
app:iconSpaceReserved="false" />
<com.philj56.gbcc.UnitSeekbarPreference
<com.philj56.gbcc.settings.UnitSeekbarPreference
android:layout="@layout/preference_widget_slider"
app:min="40"
android:max="500"
@ -40,7 +40,7 @@
app:summary="@string/settings_bluetooth_latency_summary"
app:iconSpaceReserved="false" />
<com.philj56.gbcc.preference.SliderPreference
<com.philj56.gbcc.materialPreferences.MaterialSeekbarPreference
android:layout="@layout/preference_widget_slider"
app:min="0"
android:max="100"

View File

@ -35,7 +35,7 @@
app:key="haptic_key_release"
app:title="@string/settings_haptic_key_release" />
<com.philj56.gbcc.TurboPreference
<com.philj56.gbcc.settings.TurboPreference
app:defaultValue="0"
app:iconSpaceReserved="false"
app:key="turbo_speed"

View File

@ -27,7 +27,7 @@
app:title="@string/settings_turbo_dpad"
app:summary="@string/settings_turbo_dpad_summary" />
<com.philj56.gbcc.SummaryListPreference
<com.philj56.gbcc.settings.SummaryListPreference
app:defaultValue="auto"
app:entries="@array/skin_names_array"
app:entryValues="@array/skin_values_array"
@ -35,7 +35,7 @@
app:title="@string/settings_skin"
app:iconSpaceReserved="false" />
<com.philj56.gbcc.SummaryListPreference
<com.philj56.gbcc.settings.SummaryListPreference
app:defaultValue="Light"
app:entries="@array/dmg_color_names_array"
app:entryValues="@array/dmg_color_values_array"
@ -43,7 +43,7 @@
app:title="@string/settings_dmg_color"
app:iconSpaceReserved="false" />
<com.philj56.gbcc.SummaryListPreference
<com.philj56.gbcc.settings.SummaryListPreference
app:defaultValue="Teal"
app:entries="@array/color_names_array"
app:entryValues="@array/color_values_array"
@ -51,7 +51,7 @@
app:title="@string/settings_color"
app:iconSpaceReserved="false" />
<com.philj56.gbcc.SummaryListPreference
<com.philj56.gbcc.settings.SummaryListPreference
app:defaultValue="-1"
app:entries="@array/orientation_names_array"
app:entryValues="@array/orientation_values_array"

View File

@ -22,7 +22,7 @@
app:title="@string/settings_vsync"
app:summary="@string/settings_vsync_summary"/>
<com.philj56.gbcc.SummaryListPreference
<com.philj56.gbcc.settings.SummaryListPreference
app:defaultValue="@string/settings_shader_dot_matrix"
app:entries="@array/shader_names_array"
app:entryValues="@array/shader_values_array"
@ -30,7 +30,7 @@
app:title="@string/settings_shader_dmg"
app:iconSpaceReserved="false" />
<com.philj56.gbcc.SummaryListPreference
<com.philj56.gbcc.settings.SummaryListPreference
app:defaultValue="@string/settings_shader_subpixel"
app:entries="@array/shader_names_array"
app:entryValues="@array/shader_values_array"
@ -38,7 +38,7 @@
app:title="@string/settings_shader_gbc"
app:iconSpaceReserved="false" />
<com.philj56.gbcc.SummaryListPreference
<com.philj56.gbcc.settings.SummaryListPreference
app:defaultValue="Default"
app:entries="@array/dmg_palettes_array"
app:entryValues="@array/dmg_palettes_array"

View File

@ -2,7 +2,7 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.philj56.gbcc.SummaryListPreference
<com.philj56.gbcc.settings.SummaryListPreference
app:defaultValue="Teal"
app:entries="@array/color_names_array"
app:entryValues="@array/color_values_array"
@ -10,7 +10,7 @@
app:title="@string/settings_theme"
app:iconSpaceReserved="false" />
<com.philj56.gbcc.SummaryListPreference
<com.philj56.gbcc.settings.SummaryListPreference
app:defaultValue="auto"
app:entries="@array/night_mode_names_array"
app:entryValues="@array/night_mode_values_array"
@ -25,7 +25,7 @@
app:summary="@string/settings_oled_night_mode_summary"
app:iconSpaceReserved="false" />
<com.philj56.gbcc.SummaryListPreference
<com.philj56.gbcc.settings.SummaryListPreference
app:defaultValue="back"
app:entries="@array/camera_names_array"
app:entryValues="@array/camera_values_array"
@ -33,7 +33,7 @@
app:title="@string/settings_camera"
app:iconSpaceReserved="false" />
<com.philj56.gbcc.preference.SliderPreference
<com.philj56.gbcc.materialPreferences.MaterialSeekbarPreference
android:layout="@layout/preference_widget_slider"
app:defaultValue="255"
app:min="1"

View File

@ -32,13 +32,13 @@
app:key="vsync"
app:title="@string/settings_vsync" />
<com.philj56.gbcc.TurboPreference
<com.philj56.gbcc.settings.TurboPreference
app:defaultValue="0"
app:iconSpaceReserved="false"
app:key="turbo"
app:title="@string/settings_turbo_speed" />
<com.philj56.gbcc.SummaryListPreference
<com.philj56.gbcc.settings.SummaryListPreference
app:defaultValue="Default"
app:entries="@array/dmg_palettes_array"
app:entryValues="@array/dmg_palettes_array"
@ -46,7 +46,7 @@
app:title="@string/rom_config_palette"
app:iconSpaceReserved="false" />
<com.philj56.gbcc.SummaryListPreference
<com.philj56.gbcc.settings.SummaryListPreference
app:defaultValue="@string/settings_shader_subpixel"
app:entries="@array/shader_names_array"
app:entryValues="@array/shader_values_array"