added option in settings to edit Supermodel.ini easy & raw

This commit is contained in:
izzy2lost
2026-01-20 05:09:58 -05:00
parent afd3e766b0
commit c937867bad
11 changed files with 902 additions and 1 deletions
+10
View File
@@ -26,6 +26,16 @@
android:screenOrientation="fullSensor"
android:exported="false" />
<activity
android:name=".IniEditorActivity"
android:screenOrientation="fullSensor"
android:exported="false" />
<activity
android:name=".IniSettingsActivity"
android:screenOrientation="fullSensor"
android:exported="false" />
<activity
android:name=".Super3Activity"
android:screenOrientation="sensorLandscape"
@@ -0,0 +1,100 @@
package com.izzy2lost.super3
import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import java.io.File
object IniDocumentStore {
fun ensureIniDocument(context: Context, treeUri: Uri): DocumentFile? {
val tree = DocumentFile.fromTreeUri(context, treeUri) ?: return null
val configDir =
tree.findFile("Config")?.takeIf { it.isDirectory } ?: tree.createDirectory("Config") ?: return null
val existing = findBestIniDoc(configDir)
if (existing != null) return existing
val created = configDir.createFile("application/octet-stream", "Supermodel.ini") ?: return null
seedIniDocument(context, created)
return created
}
fun readIniText(context: Context, doc: DocumentFile): String? {
return runCatching {
val input = context.contentResolver.openInputStream(doc.uri) ?: return@runCatching null
input.bufferedReader(Charsets.UTF_8).use { it.readText() }
}.getOrNull()
}
fun writeIniText(context: Context, doc: DocumentFile, text: String): Boolean {
return runCatching {
val out = context.contentResolver.openOutputStream(doc.uri) ?: return@runCatching false
out.writer(Charsets.UTF_8).use { it.write(text) }
true
}.getOrDefault(false)
}
private fun findBestIniDoc(configDir: DocumentFile): DocumentFile? {
val exact = configDir.findFile("Supermodel.ini")
if (exact != null && exact.isFile) return exact
val candidates =
configDir
.listFiles()
.filter { it.isFile }
.mapNotNull { doc ->
val name = doc.name ?: return@mapNotNull null
if (!name.startsWith("Supermodel.ini", ignoreCase = true)) return@mapNotNull null
doc
}
if (candidates.isEmpty()) return null
fun nameScore(doc: DocumentFile): Int {
val name = doc.name?.lowercase() ?: ""
return when {
name == "supermodel.ini" -> 0
name == "supermodel.ini.txt" -> 1
name.startsWith("supermodel.ini(") -> 2
else -> 3
}
}
val best =
if (candidates.any { it.lastModified() > 0L }) {
candidates.maxWithOrNull(
compareBy<DocumentFile> { it.lastModified() }
.thenBy { -nameScore(it) }
.thenBy { -(it.name?.length ?: Int.MAX_VALUE) },
)
} else {
candidates.minWithOrNull(
compareBy<DocumentFile> { nameScore(it) }
.thenBy { it.name?.length ?: Int.MAX_VALUE },
)
} ?: return null
val bestName = best.name ?: return best
if (!bestName.equals("Supermodel.ini", ignoreCase = true)) {
val renamed = runCatching { best.renameTo("Supermodel.ini") }.getOrDefault(false)
if (renamed) {
val renamedDoc = configDir.findFile("Supermodel.ini")
if (renamedDoc != null && renamedDoc.isFile) return renamedDoc
}
}
return best
}
private fun seedIniDocument(context: Context, doc: DocumentFile) {
val internal = File(File(context.getExternalFilesDir(null), "super3/Config"), "Supermodel.ini")
val input =
when {
internal.exists() -> runCatching { internal.inputStream() }.getOrNull()
else -> runCatching { context.assets.open("Config/Supermodel.ini") }.getOrNull()
}
if (input == null) return
input.use { ins ->
context.contentResolver.openOutputStream(doc.uri)?.use { outs ->
ins.copyTo(outs)
}
}
}
}
@@ -0,0 +1,171 @@
package com.izzy2lost.super3
import android.net.Uri
import android.os.Bundle
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.addTextChangedListener
import androidx.documentfile.provider.DocumentFile
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlin.concurrent.thread
class IniEditorActivity : AppCompatActivity() {
companion object {
const val EXTRA_TREE_URI = "treeUri"
}
private lateinit var toolbar: MaterialToolbar
private lateinit var pathText: TextView
private lateinit var editor: EditText
private var iniDoc: DocumentFile? = null
private var suppressDirty = false
private var dirty = false
private var busy = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
applyImmersiveMode()
setContentView(R.layout.activity_ini_editor)
toolbar = findViewById(R.id.ini_toolbar)
pathText = findViewById(R.id.ini_path)
editor = findViewById(R.id.ini_editor)
toolbar.inflateMenu(R.menu.menu_ini_editor)
toolbar.setNavigationOnClickListener { maybeExit() }
toolbar.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.action_save -> {
saveIni()
true
}
else -> false
}
}
tintMenuIcons()
updateSaveEnabled()
editor.addTextChangedListener {
if (suppressDirty || busy) return@addTextChangedListener
if (!dirty) {
dirty = true
toolbar.subtitle = "Unsaved changes"
updateSaveEnabled()
}
}
onBackPressedDispatcher.addCallback(this) {
maybeExit()
}
val treeUri = intent.getStringExtra(EXTRA_TREE_URI)?.let(Uri::parse)
if (treeUri == null) {
Toast.makeText(this, "Data folder not set", Toast.LENGTH_SHORT).show()
finish()
return
}
pathText.text = "Editing: Config/Supermodel.ini\nData folder: $treeUri"
loadIni(treeUri)
}
private fun maybeExit() {
if (busy) {
Toast.makeText(this, "Please wait...", Toast.LENGTH_SHORT).show()
return
}
if (!dirty) {
finish()
return
}
MaterialAlertDialogBuilder(this)
.setTitle("Discard changes?")
.setMessage("You have unsaved changes to Supermodel.ini.")
.setPositiveButton("Save") { _, _ -> saveIni(finishAfter = true) }
.setNegativeButton("Discard") { _, _ -> finish() }
.setNeutralButton("Cancel", null)
.show()
}
private fun loadIni(treeUri: Uri) {
setBusy(true, "Loading...")
thread(name = "Super3IniLoad") {
val doc = IniDocumentStore.ensureIniDocument(this, treeUri)
val text = doc?.let { IniDocumentStore.readIniText(this, it) }
runOnUiThread {
if (doc == null || text == null) {
setBusy(false, null)
Toast.makeText(this, "Failed to load Supermodel.ini", Toast.LENGTH_SHORT).show()
finish()
return@runOnUiThread
}
iniDoc = doc
suppressDirty = true
editor.setText(text)
editor.setSelection(0)
suppressDirty = false
dirty = false
toolbar.subtitle = null
updateSaveEnabled()
setBusy(false, null)
}
}
}
private fun saveIni(finishAfter: Boolean = false) {
val doc = iniDoc ?: return
val text = editor.text?.toString() ?: ""
setBusy(true, "Saving...")
thread(name = "Super3IniSave") {
val ok = IniDocumentStore.writeIniText(this, doc, text)
runOnUiThread {
setBusy(false, null)
if (!ok) {
Toast.makeText(this, "Failed to save Supermodel.ini", Toast.LENGTH_SHORT).show()
return@runOnUiThread
}
dirty = false
toolbar.subtitle = "Saved"
updateSaveEnabled()
if (finishAfter) {
finish()
}
}
}
}
private fun setBusy(isBusy: Boolean, status: String?) {
busy = isBusy
editor.isEnabled = !isBusy
toolbar.menu.findItem(R.id.action_save)?.isEnabled = !isBusy && dirty
if (status != null) {
toolbar.subtitle = status
} else if (!isBusy) {
toolbar.subtitle = null
}
}
private fun updateSaveEnabled() {
val saveItem = toolbar.menu.findItem(R.id.action_save)
saveItem?.isEnabled = dirty && !busy
tintMenuIcons()
}
private fun tintMenuIcons() {
val enabledColor =
MaterialColors.getColor(toolbar, com.google.android.material.R.attr.colorOnPrimary)
val disabledColor =
MaterialColors.getColor(toolbar, com.google.android.material.R.attr.colorOnSurfaceVariant)
val saveItem = toolbar.menu.findItem(R.id.action_save)
val tint = if (saveItem?.isEnabled == true) enabledColor else disabledColor
saveItem?.icon?.setTint(tint)
}
// IniDocumentStore handles SAF reads/writes + file selection.
}
@@ -0,0 +1,349 @@
package com.izzy2lost.super3
import android.net.Uri
import android.os.Bundle
import android.text.InputType
import android.widget.Toast
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import kotlin.concurrent.thread
class IniSettingsActivity : AppCompatActivity() {
private lateinit var toolbar: MaterialToolbar
private lateinit var btnPpcFrequency: MaterialButton
private lateinit var btnMultithreaded: MaterialButton
private lateinit var btnGpuMultithreaded: MaterialButton
private lateinit var btnVsync: MaterialButton
private lateinit var btnEmulateSound: MaterialButton
private lateinit var btnSoundVolume: MaterialButton
private lateinit var btnMusicVolume: MaterialButton
private var iniDoc: DocumentFile? = null
private var iniLines: MutableList<String> = mutableListOf()
private var busy = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
applyImmersiveMode()
setContentView(R.layout.activity_ini_settings)
toolbar = findViewById(R.id.ini_settings_toolbar)
btnPpcFrequency = findViewById(R.id.btn_ppc_frequency)
btnMultithreaded = findViewById(R.id.btn_multithreaded)
btnGpuMultithreaded = findViewById(R.id.btn_gpu_multithreaded)
btnVsync = findViewById(R.id.btn_vsync)
btnEmulateSound = findViewById(R.id.btn_emulate_sound)
btnSoundVolume = findViewById(R.id.btn_sound_volume)
btnMusicVolume = findViewById(R.id.btn_music_volume)
toolbar.setNavigationOnClickListener { finish() }
onBackPressedDispatcher.addCallback(this) { finish() }
val treeUri = intent.getStringExtra(IniEditorActivity.EXTRA_TREE_URI)?.let(Uri::parse)
if (treeUri == null) {
Toast.makeText(this, "Data folder not set", Toast.LENGTH_SHORT).show()
finish()
return
}
bindActions()
loadIni(treeUri)
}
private fun bindActions() {
btnPpcFrequency.setOnClickListener {
val current = readIniInt("PowerPCFrequency") ?: 50
showNumberDialog(
title = "PowerPC frequency",
key = "PowerPCFrequency",
current = current,
min = 10,
max = 200,
)
}
btnMultithreaded.setOnClickListener {
val enabled = btnMultithreaded.isChecked
applyUpdate(
updates = mapOf("MultiThreaded" to if (enabled) "1" else "0"),
onApplied = { btnMultithreaded.isChecked = enabled },
onFailed = { btnMultithreaded.isChecked = !enabled },
)
}
btnGpuMultithreaded.setOnClickListener {
val enabled = btnGpuMultithreaded.isChecked
applyUpdate(
updates = mapOf("GPUMultiThreaded" to if (enabled) "1" else "0"),
onApplied = { btnGpuMultithreaded.isChecked = enabled },
onFailed = { btnGpuMultithreaded.isChecked = !enabled },
)
}
btnVsync.setOnClickListener {
val enabled = btnVsync.isChecked
applyUpdate(
updates = mapOf("VSync" to if (enabled) "1" else "0"),
onApplied = { btnVsync.isChecked = enabled },
onFailed = { btnVsync.isChecked = !enabled },
)
}
btnEmulateSound.setOnClickListener {
val enabled = btnEmulateSound.isChecked
applyUpdate(
updates = mapOf("EmulateSound" to if (enabled) "1" else "0"),
onApplied = { btnEmulateSound.isChecked = enabled },
onFailed = { btnEmulateSound.isChecked = !enabled },
)
}
btnSoundVolume.setOnClickListener {
val current = readIniInt("SoundVolume") ?: 100
showNumberDialog(
title = "Sound volume",
key = "SoundVolume",
current = current,
min = 0,
max = 200,
)
}
btnMusicVolume.setOnClickListener {
val current = readIniInt("MusicVolume") ?: 150
showNumberDialog(
title = "Music volume",
key = "MusicVolume",
current = current,
min = 0,
max = 200,
)
}
}
private fun loadIni(treeUri: Uri) {
setBusy(true, "Loading...")
thread(name = "Super3IniSettingsLoad") {
val doc = IniDocumentStore.ensureIniDocument(this, treeUri)
val text = doc?.let { IniDocumentStore.readIniText(this, it) }
runOnUiThread {
if (doc == null || text == null) {
setBusy(false, null)
Toast.makeText(this, "Failed to load Supermodel.ini", Toast.LENGTH_SHORT).show()
finish()
return@runOnUiThread
}
iniDoc = doc
iniLines = text.split("\n").toMutableList()
applyUiFromIni()
setBusy(false, null)
}
}
}
private fun applyUiFromIni() {
val ppc = readIniInt("PowerPCFrequency") ?: 50
btnPpcFrequency.text = "PowerPC frequency: $ppc"
btnMultithreaded.isChecked = readIniBool("MultiThreaded") ?: true
btnGpuMultithreaded.isChecked = readIniBool("GPUMultiThreaded") ?: false
btnVsync.isChecked = readIniBool("VSync") ?: true
btnEmulateSound.isChecked = readIniBool("EmulateSound") ?: true
val sound = readIniInt("SoundVolume") ?: 100
val music = readIniInt("MusicVolume") ?: 150
btnSoundVolume.text = "Sound volume: $sound"
btnMusicVolume.text = "Music volume: $music"
}
private fun showNumberDialog(title: String, key: String, current: Int, min: Int, max: Int) {
val view = layoutInflater.inflate(R.layout.dialog_number_input, null)
val inputLayout = view.findViewById<TextInputLayout>(R.id.number_input_layout)
val input = view.findViewById<TextInputEditText>(R.id.number_input)
inputLayout.hint = "$title ($min-$max)"
input.inputType = InputType.TYPE_CLASS_NUMBER
input.setText(current.toString())
input.setSelection(input.text?.length ?: 0)
MaterialAlertDialogBuilder(this)
.setTitle(title)
.setView(view)
.setPositiveButton("Save") { _, _ ->
val raw = input.text?.toString()?.trim().orEmpty()
val value = raw.toIntOrNull()
if (value == null) {
Toast.makeText(this, "Enter a number between $min and $max", Toast.LENGTH_SHORT).show()
return@setPositiveButton
}
val clamped = value.coerceIn(min, max)
if (clamped != value) {
Toast.makeText(this, "Clamped to $clamped", Toast.LENGTH_SHORT).show()
}
applyUpdate(
updates = mapOf(key to clamped.toString()),
onApplied = {
when (key) {
"PowerPCFrequency" -> btnPpcFrequency.text = "PowerPC frequency: $clamped"
"SoundVolume" -> btnSoundVolume.text = "Sound volume: $clamped"
"MusicVolume" -> btnMusicVolume.text = "Music volume: $clamped"
}
},
)
}
.setNegativeButton("Cancel", null)
.show()
}
private fun applyUpdate(
updates: Map<String, String>,
onApplied: () -> Unit,
onFailed: (() -> Unit)? = null,
) {
if (busy) return
val doc = iniDoc ?: return
val updated = updateIniSection(iniLines, "global", updates)
setBusy(true, "Saving...")
thread(name = "Super3IniSettingsSave") {
val ok = IniDocumentStore.writeIniText(this, doc, updated.joinToString("\n"))
runOnUiThread {
setBusy(false, null)
if (!ok) {
Toast.makeText(this, "Failed to save Supermodel.ini", Toast.LENGTH_SHORT).show()
onFailed?.invoke()
return@runOnUiThread
}
iniLines = updated
onApplied()
Toast.makeText(this, "Saved", Toast.LENGTH_SHORT).show()
}
}
}
private fun readIniInt(key: String): Int? {
return readIniString(key)?.trim()?.toIntOrNull()
}
private fun readIniBool(key: String): Boolean? {
val v = readIniString(key)?.trim()?.lowercase() ?: return null
return when (v) {
"1", "true", "yes", "on" -> true
"0", "false", "no", "off" -> false
else -> null
}
}
private fun readIniString(key: String): String? {
val range = findSectionRange("global") ?: (0 until iniLines.size)
for (i in range) {
val line = iniLines[i]
val trimmed = line.trim()
if (trimmed.isBlank() || trimmed.startsWith(";")) continue
val rx = Regex("^\\s*${Regex.escape(key)}\\s*=", RegexOption.IGNORE_CASE)
if (rx.containsMatchIn(line)) {
val idx = line.indexOf("=")
return if (idx >= 0) line.substring(idx + 1).trim() else null
}
}
return null
}
private fun updateIniSection(
lines: List<String>,
section: String,
updates: Map<String, String>,
): MutableList<String> {
val (start, end) = findSectionBounds(lines, section) ?: run {
val out = ArrayList<String>(lines.size + updates.size + 2)
out.addAll(lines)
if (out.isNotEmpty() && out.last().isNotBlank()) out.add("")
out.add("[ Global ]")
for ((k, v) in updates) {
out.add("$k = $v")
}
return out
}
val out = ArrayList<String>(lines.size + updates.size)
out.addAll(lines.take(start + 1))
val existing = HashSet<String>(updates.size)
for (i in (start + 1) until end) {
val line = lines[i]
val trimmed = line.trim()
if (trimmed.startsWith(";") || trimmed.isBlank()) {
out.add(line)
continue
}
var replaced = false
for ((k, v) in updates) {
val rx = Regex("^\\s*${Regex.escape(k)}\\s*=", RegexOption.IGNORE_CASE)
if (rx.containsMatchIn(line)) {
out.add("$k = $v")
existing.add(k.lowercase())
replaced = true
break
}
}
if (!replaced) out.add(line)
}
for ((k, v) in updates) {
if (existing.contains(k.lowercase())) continue
out.add("$k = $v")
}
out.addAll(lines.drop(end))
return out
}
private fun findSectionRange(section: String): IntRange? {
val (start, end) = findSectionBounds(iniLines, section) ?: return null
return (start + 1) until end
}
private fun findSectionBounds(lines: List<String>, section: String): Pair<Int, Int>? {
val target = section.lowercase()
var start = -1
for (i in lines.indices) {
val name = sectionName(lines[i]) ?: continue
if (name == target) {
start = i
break
}
}
if (start < 0) return null
var end = lines.size
for (i in (start + 1) until lines.size) {
if (sectionName(lines[i]) != null) {
end = i
break
}
}
return start to end
}
private fun sectionName(line: String): String? {
val trimmed = line.trim()
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return null
return trimmed.substring(1, trimmed.length - 1).trim().lowercase()
}
private fun setBusy(isBusy: Boolean, status: String?) {
busy = isBusy
toolbar.subtitle = status ?: if (busy) toolbar.subtitle else "Easy settings"
val enabled = !isBusy
btnPpcFrequency.isEnabled = enabled
btnMultithreaded.isEnabled = enabled
btnGpuMultithreaded.isEnabled = enabled
btnVsync.isEnabled = enabled
btnEmulateSound.isEnabled = enabled
btnSoundVolume.isEnabled = enabled
btnMusicVolume.isEnabled = enabled
}
}
@@ -173,6 +173,7 @@ class MainActivity : AppCompatActivity() {
val btnShowShifterOverlay: MaterialButton = headerView.findViewById(R.id.btn_show_shifter_overlay)
val btnGyroSteering: MaterialButton = headerView.findViewById(R.id.btn_gyro_steering)
val btnGyroSensitivity: MaterialButton = headerView.findViewById(R.id.btn_gyro_sensitivity)
val btnEditSupermodelIni: MaterialButton = headerView.findViewById(R.id.btn_edit_supermodel_ini)
gamesAdapter = GamesAdapter { item ->
if (!item.launchable) {
@@ -197,6 +198,31 @@ class MainActivity : AppCompatActivity() {
btnPickUserFolder.setOnClickListener { pickUserFolder.launch(null) }
btnRescan.setOnClickListener { refreshUi() }
btnEditSupermodelIni.setOnClickListener {
val tree = userTreeUri
if (tree == null) {
Toast.makeText(this, "Pick a data folder first", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
drawerLayout.closeDrawer(GravityCompat.START)
val options = arrayOf("Easy settings", "Raw text")
MaterialAlertDialogBuilder(this)
.setTitle("Edit Supermodel.ini")
.setItems(options) { _, which ->
val target =
when (which) {
0 -> IniSettingsActivity::class.java
else -> IniEditorActivity::class.java
}
val intent =
Intent(this, target).apply {
putExtra(IniEditorActivity.EXTRA_TREE_URI, tree.toString())
}
startActivity(intent)
}
.show()
}
bindVideoSettingsUi()
bindTimingUi()
@@ -83,7 +83,7 @@ object UserDataSync {
private fun copyFileToDocFile(resolver: ContentResolver, from: File, toDir: DocumentFile) {
val existing = toDir.findFile(from.name)
val outDoc = existing ?: toDir.createFile("application/octet-stream", from.name) ?: return
resolver.openOutputStream(outDoc.uri, "wt")?.use { output ->
resolver.openOutputStream(outDoc.uri)?.use { output ->
from.inputStream().use { input ->
input.copyTo(output)
}
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/ini_toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="@drawable/exit_to_app_24px"
app:navigationIconTint="?attr/colorOnPrimary"
app:title="Supermodel.ini"
app:titleTextColor="?attr/colorOnPrimary"
app:subtitleTextColor="?attr/colorOnPrimary" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/ini_path"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="12dp"
android:ellipsize="middle"
android:maxLines="2"
android:text="Editing: Config/Supermodel.ini"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ini_toolbar" />
<EditText
android:id="@+id/ini_editor"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:background="@drawable/setup_wizard_path_background"
android:fontFamily="monospace"
android:gravity="top|start"
android:fadingEdgeLength="12dp"
android:inputType="textMultiLine|textNoSuggestions"
android:overScrollMode="ifContentScrolls"
android:scrollbars="vertical"
android:scrollbarStyle="insideInset"
android:scrollbarFadeDuration="200"
android:scrollbarDefaultDelayBeforeFade="600"
android:textColor="?attr/colorOnSurface"
android:textColorHint="?attr/colorOnSurfaceVariant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ini_path" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,134 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/ini_settings_toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="@drawable/exit_to_app_24px"
app:navigationIconTint="?attr/colorOnPrimary"
app:title="Supermodel.ini"
app:subtitle="Easy settings"
app:titleTextColor="?attr/colorOnPrimary"
app:subtitleTextColor="?attr/colorOnPrimary" />
<androidx.core.widget.NestedScrollView
android:id="@+id/ini_settings_scroll"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:clipToPadding="false"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingBottom="24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ini_settings_toolbar">
<LinearLayout
android:id="@+id/ini_settings_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/ini_settings_perf_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Performance"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnSurface" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_ppc_frequency"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="PowerPC frequency: 50"
style="?attr/materialButtonElevatedStyle" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_multithreaded"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:checkable="true"
android:text="Multi-threaded (CPU)"
style="?attr/materialButtonElevatedStyle" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_gpu_multithreaded"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:checkable="true"
android:text="Multi-threaded (GPU)"
style="?attr/materialButtonElevatedStyle" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/ini_settings_display_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="Display"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnSurface" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_vsync"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:checkable="true"
android:text="VSync"
style="?attr/materialButtonElevatedStyle" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/ini_settings_audio_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="Audio"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnSurface" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_emulate_sound"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:checkable="true"
android:text="Emulate sound"
style="?attr/materialButtonElevatedStyle" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_sound_volume"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Sound volume: 100"
style="?attr/materialButtonElevatedStyle" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_music_volume"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Music volume: 150"
style="?attr/materialButtonElevatedStyle" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/number_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/number_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>
@@ -172,6 +172,24 @@
android:text="Gyro sensitivity: Normal"
style="?attr/materialButtonElevatedStyle" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/advanced_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Advanced"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnPrimary" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_edit_supermodel_ini"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Edit Supermodel.ini"
app:icon="@drawable/hard_drive_24px"
style="?attr/materialButtonElevatedStyle" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/games_folder_text"
android:layout_width="match_parent"
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_save"
android:icon="@drawable/save_24px"
android:title="Save"
app:showAsAction="always" />
</menu>