Rewrite adapter to handle any layout

This commit is contained in:
Willi Ye 2020-10-03 11:58:34 +02:00 committed by ◱ PixelyIon
parent e023dbbf0a
commit 22140defae
33 changed files with 843 additions and 811 deletions

View File

@ -59,6 +59,13 @@
android:name="android.support.PARENT_ACTIVITY"
android:value="emu.skyline.SettingsActivity" />
</activity>
<activity
android:name="emu.skyline.input.onscreen.OnScreenEditActivity"
android:exported="true">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="emu.skyline.input.ControllerActivity" />
</activity>
<activity
android:name="emu.skyline.EmulationActivity"
android:configChanges="orientation|screenSize"

View File

@ -16,10 +16,11 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import emu.skyline.adapter.LogAdapter
import emu.skyline.adapter.GenericAdapter
import emu.skyline.adapter.HeaderViewItem
import emu.skyline.adapter.LogViewItem
import kotlinx.android.synthetic.main.log_activity.*
import kotlinx.android.synthetic.main.titlebar.*
import org.json.JSONObject
@ -38,7 +39,7 @@ class LogActivity : AppCompatActivity() {
/**
* The adapter used for adding elements from the log to [log_list]
*/
private lateinit var adapter : LogAdapter
private val adapter = GenericAdapter()
/**
* This initializes [toolbar] and fills [log_list] with data from the logs
@ -54,11 +55,9 @@ class LogActivity : AppCompatActivity() {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val compact = prefs.getBoolean("log_compact", false)
val logLevel = prefs.getString("log_level", "3")!!.toInt()
adapter = LogAdapter(this, compact, logLevel, resources.getStringArray(R.array.log_level))
val logLevels = resources.getStringArray(R.array.log_level)
log_list.adapter = adapter
log_list.layoutManager = LinearLayoutManager(this)
if (!compact)
log_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
@ -66,8 +65,21 @@ class LogActivity : AppCompatActivity() {
try {
logFile = File(applicationContext.filesDir.canonicalPath + "/skyline.log")
logFile.forEachLine {
adapter.add(it)
logFile.forEachLine { logLine ->
try {
val logMeta = logLine.split("|", limit = 3)
if (logMeta[0].startsWith("1")) {
val level = logMeta[1].toInt()
if (level > logLevel) return@forEachLine
adapter.addItem(LogViewItem(compact, logMeta[2].replace('\\', '\n'), logLevels[level]))
} else {
adapter.addItem(HeaderViewItem(logMeta[1]))
}
} catch (ignored : IndexOutOfBoundsException) {
} catch (ignored : NumberFormatException) {
}
}
} catch (e : FileNotFoundException) {
Log.w("Logger", "IO Error during access of log file: " + e.message)
@ -149,10 +161,10 @@ class LogActivity : AppCompatActivity() {
urlConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8")
urlConnection.setRequestProperty("Referer", "https://hastebin.com/")
val bufferedWriter = urlConnection.outputStream.bufferedWriter()
bufferedWriter.write(logFile.readText())
bufferedWriter.flush()
bufferedWriter.close()
urlConnection.outputStream.bufferedWriter().use {
it.write(logFile.readText())
it.flush()
}
if (urlConnection.responseCode != 200) {
Log.e("LogUpload", "HTTPS Status Code: " + urlConnection.responseCode)

View File

@ -18,17 +18,21 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.SearchView
import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import emu.skyline.adapter.AppAdapter
import emu.skyline.adapter.GridLayoutSpan
import emu.skyline.adapter.AppViewItem
import emu.skyline.adapter.GenericAdapter
import emu.skyline.adapter.HeaderViewItem
import emu.skyline.adapter.LayoutType
import emu.skyline.data.AppItem
import emu.skyline.data.BaseElement
import emu.skyline.data.BaseHeader
import emu.skyline.loader.LoaderResult
import emu.skyline.loader.RomFile
import emu.skyline.loader.RomFormat
@ -41,6 +45,10 @@ import kotlin.concurrent.thread
import kotlin.math.ceil
class MainActivity : AppCompatActivity() {
companion object {
private val TAG = MainActivity::class.java.simpleName
}
/**
* This is used to get/set shared preferences
*/
@ -49,29 +57,38 @@ class MainActivity : AppCompatActivity() {
/**
* The adapter used for adding elements to [app_list]
*/
private lateinit var adapter : AppAdapter
private val adapter = GenericAdapter()
private var reloading = AtomicBoolean()
private val layoutType get() = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()]
private val missingIcon by lazy { ContextCompat.getDrawable(this, R.drawable.default_icon)!!.toBitmap(256, 256) }
private fun AppItem.toViewItem() = AppViewItem(layoutType, this, missingIcon, ::selectStartGame, ::selectShowGameDialog)
/**
* This adds all files in [directory] with [extension] as an entry in [adapter] using [RomFile] to load metadata
*/
private fun addEntries(extension : String, romFormat : RomFormat, directory : DocumentFile, found : Boolean = false) : Boolean {
private fun addEntries(extension : String, romFormat : RomFormat, directory : DocumentFile, romElements : ArrayList<BaseElement>, found : Boolean = false) : Boolean {
var foundCurrent = found
directory.listFiles().forEach { file ->
if (file.isDirectory) {
foundCurrent = addEntries(extension, romFormat, file, foundCurrent)
foundCurrent = addEntries(extension, romFormat, file, romElements, foundCurrent)
} else {
if (extension.equals(file.name?.substringAfterLast("."), ignoreCase = true)) {
RomFile(this, romFormat, file.uri).let { romFile ->
val finalFoundCurrent = foundCurrent
runOnUiThread {
if (!finalFoundCurrent) adapter.addHeader(romFormat.name)
if (!finalFoundCurrent) {
romElements.add(BaseHeader(romFormat.name))
adapter.addItem(HeaderViewItem(romFormat.name))
}
adapter.addItem(AppItem(romFile.appEntry))
romElements.add(AppItem(romFile.appEntry).also {
adapter.addItem(it.toViewItem())
})
}
foundCurrent = true
@ -86,15 +103,22 @@ class MainActivity : AppCompatActivity() {
/**
* This refreshes the contents of the adapter by either trying to load cached adapter data or searches for them to recreate a list
*
* @param tryLoad If this is false then trying to load cached adapter data is skipped entirely
* @param loadFromFile If this is false then trying to load cached adapter data is skipped entirely
*/
private fun refreshAdapter(tryLoad : Boolean) {
if (tryLoad) {
private fun refreshAdapter(loadFromFile : Boolean) {
val romsFile = File(applicationContext.filesDir.canonicalPath + "/roms.bin")
if (loadFromFile) {
try {
adapter.load(File(applicationContext.filesDir.canonicalPath + "/roms.bin"))
loadSerializedList<BaseElement>(romsFile).forEach {
if (it is BaseHeader)
adapter.addItem(HeaderViewItem(it.title))
else if (it is AppItem)
adapter.addItem(it.toViewItem())
}
return
} catch (e : Exception) {
Log.w("refreshFiles", "Ran into exception while loading: ${e.message}")
Log.w(TAG, "Ran into exception while loading: ${e.message}")
}
}
@ -107,22 +131,26 @@ class MainActivity : AppCompatActivity() {
}
try {
runOnUiThread { adapter.clear() }
runOnUiThread { adapter.removeAllItems() }
val searchLocation = DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!
var foundRoms = addEntries("nro", RomFormat.NRO, searchLocation)
foundRoms = foundRoms or addEntries("nso", RomFormat.NSO, searchLocation)
foundRoms = foundRoms or addEntries("nca", RomFormat.NCA, searchLocation)
foundRoms = foundRoms or addEntries("nsp", RomFormat.NSP, searchLocation)
val romElements = ArrayList<BaseElement>()
addEntries("nro", RomFormat.NRO, searchLocation, romElements)
addEntries("nso", RomFormat.NSO, searchLocation, romElements)
addEntries("nca", RomFormat.NCA, searchLocation, romElements)
addEntries("nsp", RomFormat.NSP, searchLocation, romElements)
runOnUiThread {
if (!foundRoms) adapter.addHeader(getString(R.string.no_rom))
if (romElements.isEmpty()) {
romElements.add(BaseHeader(getString(R.string.no_rom)))
adapter.addItem(HeaderViewItem(getString(R.string.no_rom)))
}
try {
adapter.save(File(applicationContext.filesDir.canonicalPath + "/roms.bin"))
romElements.serialize(romsFile)
} catch (e : IOException) {
Log.w("refreshFiles", "Ran into exception while saving: ${e.message}")
Log.w(TAG, "Ran into exception while saving: ${e.message}")
}
}
@ -216,22 +244,27 @@ class MainActivity : AppCompatActivity() {
}
}
private fun setAppListDecoration() {
when (layoutType) {
LayoutType.List -> app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
LayoutType.Grid, LayoutType.GridCompact -> if (app_list.itemDecorationCount > 0) app_list.removeItemDecorationAt(0)
}
}
private fun setupAppList() {
app_list.adapter = adapter
val itemWidth = 225
val metrics = resources.displayMetrics
val gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt()
adapter = AppAdapter(layoutType = layoutType, onClick = ::selectStartGame, onLongClick = ::selectShowGameDialog)
app_list.adapter = adapter
app_list.layoutManager = when (adapter.layoutType) {
LayoutType.List -> LinearLayoutManager(this).also { app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL)) }
LayoutType.Grid, LayoutType.GridCompact -> GridLayoutManager(this, gridSpan).apply {
spanSizeLookup = GridLayoutSpan(adapter, gridSpan).also {
if (app_list.itemDecorationCount > 0) app_list.removeItemDecorationAt(0)
}
app_list.layoutManager = GridLayoutManager(this, gridSpan).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position : Int) = if (layoutType == LayoutType.List || adapter.currentItems[position] is HeaderViewItem) gridSpan else 1
}
}
setAppListDecoration()
if (sharedPreferences.getString("search_location", "") == "") {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
@ -337,8 +370,19 @@ class MainActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
if (layoutType != adapter.layoutType) {
setupAppList()
var layoutTypeChanged = false
for (appViewItem in adapter.allItems.filterIsInstance(AppViewItem::class.java)) {
if (layoutType != appViewItem.layoutType) {
appViewItem.layoutType = layoutType
layoutTypeChanged = true
} else {
break
}
}
if (layoutTypeChanged) {
adapter.notifyAllItemsChanged()
setAppListDecoration()
}
val gridCardMagin = resources.getDimensionPixelSize(R.dimen.app_card_margin_half)

View File

@ -0,0 +1,22 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline
import java.io.File
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.Serializable
fun <T : Serializable> ArrayList<T>.serialize(file : File) {
ObjectOutputStream(file.outputStream()).use {
it.writeObject(this)
}
}
@Suppress("UNCHECKED_CAST")
fun <T : Serializable> loadSerializedList(file : File) = ObjectInputStream(file.inputStream()).use {
it.readObject()
} as ArrayList<T>

View File

@ -1,114 +0,0 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.adapter
import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.ImageView
import android.widget.RelativeLayout
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.recyclerview.widget.RecyclerView
import emu.skyline.R
import emu.skyline.data.AppItem
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.app_item_grid_compact.*
/**
* This enumerates the type of layouts the menu can be in
*/
enum class LayoutType(val layoutRes : Int) {
List(R.layout.app_item_linear),
Grid(R.layout.app_item_grid),
GridCompact(R.layout.app_item_grid_compact)
}
private typealias InteractionFunction = (appItem : AppItem) -> Unit
/**
* This adapter is used to display all found applications using their metadata
*/
internal class AppAdapter(val layoutType : LayoutType, private val onClick : InteractionFunction, private val onLongClick : InteractionFunction) : HeaderAdapter<AppItem, BaseHeader, RecyclerView.ViewHolder>() {
private lateinit var context : Context
private val missingIcon by lazy { ContextCompat.getDrawable(context, R.drawable.default_icon)!!.toBitmap(256, 256) }
/**
* This adds a header to the view with the contents of [string]
*/
fun addHeader(string : String) {
super.addHeader(BaseHeader(string))
}
private class ItemViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
private class HeaderViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
/**
* This function creates the view-holder of type [viewType] with the layout parent as [parent]
*/
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) : RecyclerView.ViewHolder {
context = parent.context
val inflater = LayoutInflater.from(context)
val view = when (ElementType.values()[viewType]) {
ElementType.Item -> inflater.inflate(layoutType.layoutRes, parent, false)
ElementType.Header -> inflater.inflate(R.layout.section_item, parent, false)
}
return when (ElementType.values()[viewType]) {
ElementType.Item -> ItemViewHolder(view)
ElementType.Header -> HeaderViewHolder(view)
}
}
/**
* This function binds the item at [position] to the supplied [holder]
*/
override fun onBindViewHolder(holder : RecyclerView.ViewHolder, position : Int) {
val item = getItem(position)
if (item is AppItem && holder is ItemViewHolder) {
holder.text_title.text = item.title
holder.text_subtitle.text = item.subTitle ?: item.loaderResultString(holder.text_subtitle.context)
holder.icon.setImageBitmap(item.icon ?: missingIcon)
if (layoutType == LayoutType.List) {
holder.icon.setOnClickListener { showIconDialog(item) }
}
when (layoutType) {
LayoutType.List -> holder.itemView
LayoutType.Grid, LayoutType.GridCompact -> holder.card_app_item_grid
}.apply {
setOnClickListener { onClick.invoke(item) }
setOnLongClickListener { true.also { onLongClick.invoke(item) } }
}
} else if (item is BaseHeader && holder is HeaderViewHolder) {
holder.text_title.text = item.title
}
}
private fun showIconDialog(appItem : AppItem) {
val builder = Dialog(context)
builder.requestWindowFeature(Window.FEATURE_NO_TITLE)
builder.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
val imageView = ImageView(context)
imageView.setImageBitmap(appItem.icon ?: missingIcon)
builder.addContentView(imageView, RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
builder.show()
}
}

View File

@ -0,0 +1,73 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.adapter
import android.app.Dialog
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.ImageView
import android.widget.RelativeLayout
import emu.skyline.R
import emu.skyline.data.AppItem
import kotlinx.android.synthetic.main.app_item_grid_compact.*
/**
* This enumerates the type of layouts the menu can be in
*/
enum class LayoutType(val layoutRes : Int) {
List(R.layout.app_item_linear),
Grid(R.layout.app_item_grid),
GridCompact(R.layout.app_item_grid_compact)
}
private typealias InteractionFunction = (appItem : AppItem) -> Unit
private data class AppLayoutFactory(private val layoutType : LayoutType) : GenericLayoutFactory {
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(layoutType.layoutRes, parent, false)
}
class AppViewItem(var layoutType : LayoutType, private val item : AppItem, private val missingIcon : Bitmap, private val onClick : InteractionFunction, private val onLongClick : InteractionFunction) : GenericViewHolderBinder() {
override fun getLayoutFactory() : GenericLayoutFactory = AppLayoutFactory(layoutType)
override fun bind(holder : GenericViewHolder, position : Int) {
holder.text_title.text = item.title
holder.text_subtitle.text = item.subTitle ?: item.loaderResultString(holder.text_subtitle.context)
holder.icon.setImageBitmap(item.icon ?: missingIcon)
if (layoutType == LayoutType.List) {
holder.icon.setOnClickListener { showIconDialog(holder.icon.context, item) }
}
when (layoutType) {
LayoutType.List -> holder.itemView
LayoutType.Grid, LayoutType.GridCompact -> holder.card_app_item_grid
}.apply {
setOnClickListener { onClick.invoke(item) }
setOnLongClickListener { true.also { onLongClick.invoke(item) } }
}
}
private fun showIconDialog(context : Context, appItem : AppItem) {
val builder = Dialog(context)
builder.requestWindowFeature(Window.FEATURE_NO_TITLE)
builder.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
val imageView = ImageView(context)
imageView.setImageBitmap(appItem.icon ?: missingIcon)
builder.addContentView(imageView, RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
builder.show()
}
override fun toString() = item.key()
}

View File

@ -1,189 +0,0 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.adapter
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import emu.skyline.R
import emu.skyline.data.BaseItem
import emu.skyline.input.*
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.controller_item.*
import kotlinx.android.synthetic.main.section_item.text_title
/**
* This is a class that holds everything relevant to a single item in the controller configuration list
*
* @param content The main line of text describing what the item is
* @param subContent The secondary line of text to show data more specific data about the item
*/
abstract class ControllerItem(var content : String, var subContent : String) : BaseItem() {
lateinit var adapter : ControllerAdapter
var position : Int? = null
/**
* This function updates the visible contents of the item
*/
fun update(content : String?, subContent : String?) {
if (content != null)
this.content = content
if (subContent != null)
this.subContent = subContent
position?.let { adapter.notifyItemChanged(it) }
}
/**
* This is used as a generic function to update the contents of the item
*/
abstract fun update()
}
/**
* This item is used to display the [type] of the currently active controller
*/
class ControllerTypeItem(val context : Context, val type : ControllerType) : ControllerItem(context.getString(R.string.controller_type), context.getString(type.stringRes)) {
/**
* This function just updates [subContent] based on [type]
*/
override fun update() = update(null, context.getString(type.stringRes))
}
/**
* This item is used to display general settings items regarding controller
*
* @param type The type of controller setting this item is displaying
*/
class ControllerGeneralItem(val context : ControllerActivity, val type : GeneralType) : ControllerItem(context.getString(type.stringRes), getSummary(context, type)) {
companion object {
/**
* This returns the summary for [type] by using data encapsulated within [Controller]
*/
fun getSummary(context : ControllerActivity, type : GeneralType) : String {
val controller = InputManager.controllers[context.id]!!
return when (type) {
GeneralType.PartnerJoyCon -> {
val partner = (controller as JoyConLeftController).partnerId
if (partner != null)
"${context.getString(R.string.controller)} #${partner + 1}"
else
context.getString(R.string.none)
}
GeneralType.RumbleDevice -> controller.rumbleDeviceName ?: context.getString(R.string.none)
}
}
}
/**
* This function just updates [subContent] based on [getSummary]
*/
override fun update() = update(null, getSummary(context, type))
}
/**
* This item is used to display a particular [button] mapping for the controller
*/
class ControllerButtonItem(val context : ControllerActivity, val button : ButtonId) : ControllerItem(button.long?.let { context.getString(it) } ?: button.toString(), getSummary(context, button)) {
companion object {
/**
* This returns the summary for [button] by doing a reverse-lookup in [InputManager.eventMap]
*/
fun getSummary(context : ControllerActivity, button : ButtonId) : String {
val guestEvent = ButtonGuestEvent(context.id, button)
return InputManager.eventMap.filter { it.value is ButtonGuestEvent && it.value == guestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
}
}
/**
* This function just updates [subContent] based on [getSummary]
*/
override fun update() = update(null, getSummary(context, button))
}
/**
* This item is used to display all information regarding a [stick] and it's mappings for the controller
*/
class ControllerStickItem(val context : ControllerActivity, val stick : StickId) : ControllerItem(stick.toString(), getSummary(context, stick)) {
companion object {
/**
* This returns the summary for [stick] by doing reverse-lookups in [InputManager.eventMap]
*/
fun getSummary(context : ControllerActivity, stick : StickId) : String {
val buttonGuestEvent = ButtonGuestEvent(context.id, stick.button)
val button = InputManager.eventMap.filter { it.value is ButtonGuestEvent && it.value == buttonGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
var axisGuestEvent = AxisGuestEvent(context.id, stick.yAxis, true)
val yAxisPlus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
axisGuestEvent = AxisGuestEvent(context.id, stick.yAxis, false)
val yAxisMinus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
axisGuestEvent = AxisGuestEvent(context.id, stick.xAxis, true)
val xAxisPlus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
axisGuestEvent = AxisGuestEvent(context.id, stick.xAxis, false)
val xAxisMinus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
return "${context.getString(R.string.button)}: $button\n${context.getString(R.string.up)}: $yAxisPlus\n${context.getString(R.string.down)}: $yAxisMinus\n${context.getString(R.string.left)}: $xAxisMinus\n${context.getString(R.string.right)}: $xAxisPlus"
}
}
/**
* This function just updates [subContent] based on [getSummary]
*/
override fun update() = update(null, getSummary(context, stick))
}
class ControllerCheckBox()
/**
* This adapter is used to create a list which handles having a simple view
*/
class ControllerAdapter(private val onItemClickCallback : (item : ControllerItem) -> Unit) : HeaderAdapter<ControllerItem?, BaseHeader, RecyclerView.ViewHolder>() {
fun addHeader(string : String) {
super.addHeader(BaseHeader(string))
}
fun addItem(item : ControllerItem) {
item.adapter = this
super.addItem(item)
}
private class ItemViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
private class HeaderViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) : RecyclerView.ViewHolder = LayoutInflater.from(parent.context).let { layoutInflater ->
when (ElementType.values()[viewType]) {
ElementType.Header -> HeaderViewHolder(layoutInflater.inflate(R.layout.section_item, parent, false))
ElementType.Item -> ItemViewHolder(layoutInflater.inflate(R.layout.controller_item, parent, false))
}
}
override fun onBindViewHolder(holder : RecyclerView.ViewHolder, position : Int) {
val item = getItem(position)
if (item is ControllerItem && holder is ItemViewHolder) {
item.position = position
holder.text_title.text = item.content
holder.text_subtitle.text = item.subContent
holder.itemView.setOnClickListener { onItemClickCallback.invoke(item) }
} else if (item is BaseHeader && holder is HeaderViewHolder) {
holder.text_title.text = item.title
}
}
}

View File

@ -0,0 +1,116 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.adapter
import android.view.ViewGroup
import android.widget.Filter
import android.widget.Filterable
import androidx.recyclerview.widget.RecyclerView
import info.debatty.java.stringsimilarity.Cosine
import info.debatty.java.stringsimilarity.JaroWinkler
import java.util.*
class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
var currentSearchTerm = ""
val currentItems get() = if (currentSearchTerm.isEmpty()) allItems else filteredItems
val allItems = mutableListOf<GenericViewHolderBinder>()
private var filteredItems = listOf<GenericViewHolderBinder>()
private val viewTypesMapping = mutableMapOf<GenericLayoutFactory, Int>()
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) = GenericViewHolder(viewTypesMapping.filterValues { it == viewType }.keys.single().createLayout(parent))
override fun onBindViewHolder(holder : GenericViewHolder, position : Int) {
currentItems[position].apply {
adapter = this@GenericAdapter
bind(holder, position)
}
}
override fun getItemCount() = currentItems.size
override fun getItemViewType(position : Int) = viewTypesMapping.getOrPut(currentItems[position].getLayoutFactory(), { viewTypesMapping.size })
fun addItem(item : GenericViewHolderBinder) {
allItems.add(item)
notifyItemInserted(currentItems.size)
}
fun removeAllItems() {
val size = currentItems.size
allItems.clear()
notifyItemRangeRemoved(0, size)
}
fun notifyAllItemsChanged() {
notifyItemRangeChanged(0, currentItems.size)
}
/**
* This returns an instance of the filter object which is used to search for items in the view
*/
override fun getFilter() = object : Filter() {
/**
* We use Jaro-Winkler distance for string similarity (https://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance)
*/
private val jw = JaroWinkler()
/**
* We use Cosine similarity for string similarity (https://en.wikipedia.org/wiki/Cosine_similarity)
*/
private val cos = Cosine()
inner class ScoredItem(val score : Double, val item : GenericViewHolderBinder)
/**
* This sorts the items in [allItems] in relation to how similar they are to [currentSearchTerm]
*/
fun extractSorted() = allItems.mapNotNull { item ->
item.toString().toLowerCase(Locale.getDefault()).let {
val similarity = (jw.similarity(currentSearchTerm, it)) + cos.similarity(currentSearchTerm, it) / 2
if (similarity != 0.0) ScoredItem(similarity, item) else null
}
}.apply {
sortedByDescending { it.score }
}
/**
* This performs filtering on the items in [allItems] based on similarity to [term]
*/
override fun performFiltering(term : CharSequence) : FilterResults {
val results = FilterResults()
currentSearchTerm = (term as String).toLowerCase(Locale.getDefault())
if (term.isEmpty()) {
results.values = allItems.toMutableList()
results.count = allItems.size
} else {
val filterData = mutableListOf<GenericViewHolderBinder>()
val topResults = extractSorted()
val avgScore = topResults.sumByDouble { it.score } / topResults.size
for (result in topResults)
if (result.score > avgScore) filterData.add(result.item)
results.values = filterData
results.count = filterData.size
}
return results
}
/**
* This publishes the results that were calculated in [performFiltering] to the view
*/
override fun publishResults(charSequence : CharSequence, results : FilterResults) {
@Suppress("UNCHECKED_CAST")
filteredItems = results.values as List<GenericViewHolderBinder>
notifyDataSetChanged()
}
}
}

View File

@ -0,0 +1,25 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.adapter
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer
class GenericViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
interface GenericLayoutFactory {
fun createLayout(parent : ViewGroup) : View
}
abstract class GenericViewHolderBinder {
var adapter : GenericAdapter? = null
abstract fun getLayoutFactory() : GenericLayoutFactory
abstract fun bind(holder : GenericViewHolder, position : Int)
}

View File

@ -1,247 +0,0 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.adapter
import android.util.SparseIntArray
import android.widget.Filter
import android.widget.Filterable
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import emu.skyline.data.BaseItem
import info.debatty.java.stringsimilarity.Cosine
import info.debatty.java.stringsimilarity.JaroWinkler
import java.io.*
import java.util.*
import kotlin.collections.ArrayList
/**
* An enumeration of the type of elements in this adapter
*/
enum class ElementType {
Header,
Item,
}
/**
* This is an abstract class that all adapter element classes inherit from
*/
abstract class BaseElement constructor(val elementType : ElementType) : Serializable
/**
* This is an abstract class that all adapter header classes inherit from
*/
class BaseHeader constructor(val title : String) : BaseElement(ElementType.Header)
/**
* This adapter has the ability to have 2 types of elements specifically headers and items
*/
abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHeader?, ViewHolder : RecyclerView.ViewHolder?> : RecyclerView.Adapter<ViewHolder>(), Filterable, Serializable {
/**
* This holds all the elements in an array even if they may not be visible
*/
var elementArray : ArrayList<BaseElement?> = ArrayList()
/**
* This holds the indices of all the visible items in [elementArray]
*/
var visibleArray : ArrayList<Int> = ArrayList()
/**
* This holds the search term if there is any, to filter any items added during a search
*/
private var searchTerm = ""
/**
* This functions adds [item] to [elementArray] and [visibleArray] based on the filter
*/
fun addItem(item : ItemType) {
elementArray.add(item)
if (searchTerm.isNotEmpty()) {
filter.filter(searchTerm)
} else {
visibleArray.add(elementArray.size - 1)
notifyItemInserted(visibleArray.size)
}
}
/**
* This function adds [header] to [elementArray] and [visibleArray] based on if the filter is active
*/
fun addHeader(header : HeaderType) {
elementArray.add(header)
if (searchTerm.isEmpty()) {
visibleArray.add(elementArray.size - 1)
notifyItemInserted(visibleArray.size)
}
}
/**
* This serializes [elementArray] into [file]
*/
@Throws(IOException::class)
fun save(file : File) {
val fileObj = FileOutputStream(file)
val out = ObjectOutputStream(fileObj)
out.writeObject(elementArray)
out.close()
fileObj.close()
}
/**
* This reads in [elementArray] from [file]
*/
@Throws(IOException::class, ClassNotFoundException::class)
open fun load(file : File) {
val fileObj = FileInputStream(file)
val input = ObjectInputStream(fileObj)
@Suppress("UNCHECKED_CAST")
elementArray = input.readObject() as ArrayList<BaseElement?>
input.close()
fileObj.close()
filter.filter(searchTerm)
}
/**
* This clears the view by clearing [elementArray] and [visibleArray]
*/
fun clear() {
elementArray.clear()
visibleArray.clear()
notifyDataSetChanged()
}
/**
* This returns the amount of elements that should be drawn to the list
*/
override fun getItemCount() : Int = visibleArray.size
/**
* This returns a particular element at [position]
*/
fun getItem(position : Int) : BaseElement? {
return elementArray[visibleArray[position]]
}
/**
* This returns the type of an element at the specified position
*
* @param position The position of the element
*/
override fun getItemViewType(position : Int) : Int {
return elementArray[visibleArray[position]]!!.elementType.ordinal
}
/**
* This returns an instance of the filter object which is used to search for items in the view
*/
override fun getFilter() = object : Filter() {
/**
* We use Jaro-Winkler distance for string similarity (https://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance)
*/
private val jw = JaroWinkler()
/**
* We use Cosine similarity for string similarity (https://en.wikipedia.org/wiki/Cosine_similarity)
*/
private val cos = Cosine()
/**
* This class is used to store the results of the item sorting
*
* @param score The score of this result
* @param index The index of this item
*/
inner class ScoredItem(val score : Double, val index : Int) {}
/**
* This sorts the items in [keyArray] in relation to how similar they are to [term]
*/
fun extractSorted(term : String, keyArray : ArrayList<String>) : Array<ScoredItem> {
val scoredItems : MutableList<ScoredItem> = ArrayList()
keyArray.forEachIndexed { index, item ->
val similarity = (jw.similarity(term, item) + cos.similarity(term, item)) / 2
if (similarity != 0.0)
scoredItems.add(ScoredItem(similarity, index))
}
scoredItems.sortWith(compareByDescending { it.score })
return scoredItems.toTypedArray()
}
/**
* This performs filtering on the items in [elementArray] based on similarity to [term]
*/
override fun performFiltering(term : CharSequence) : FilterResults {
val results = FilterResults()
searchTerm = (term as String).toLowerCase(Locale.getDefault())
if (term.isEmpty()) {
results.values = elementArray.indices.toMutableList()
results.count = elementArray.size
} else {
val filterData = ArrayList<Int>()
val keyArray = ArrayList<String>()
val keyIndex = SparseIntArray()
for (index in elementArray.indices) {
val item = elementArray[index]!!
if (item is BaseItem && item.key() != null) {
keyIndex.append(keyArray.size, index)
keyArray.add(item.key()!!.toLowerCase(Locale.getDefault()))
}
}
val topResults = extractSorted(searchTerm, keyArray)
val avgScore = topResults.sumByDouble { it.score } / topResults.size
for (result in topResults)
if (result.score > avgScore)
filterData.add(keyIndex[result.index])
results.values = filterData
results.count = filterData.size
}
return results
}
/**
* This publishes the results that were calculated in [performFiltering] to the view
*/
override fun publishResults(charSequence : CharSequence, results : FilterResults) {
if (results.values is ArrayList<*>) {
@Suppress("UNCHECKED_CAST")
visibleArray = results.values as ArrayList<Int>
notifyDataSetChanged()
}
}
}
}
/**
* This class is used to lookup the span based on the type of the element
*
* @param adapter The adapter which is used to deduce the type of the item based on the position
* @param headerSpan The span size to return for headers
*/
class GridLayoutSpan<ItemType : BaseItem?, HeaderType : BaseHeader?, ViewHolder : RecyclerView.ViewHolder?>(val adapter : HeaderAdapter<ItemType, HeaderType, ViewHolder>, private val headerSpan : Int) : GridLayoutManager.SpanSizeLookup() {
/**
* This returns the size of the span based on the type of the element at [position]
*/
override fun getSpanSize(position : Int) : Int {
val item = adapter.getItem(position)!!
return if (item.elementType == ElementType.Item)
1
else
headerSpan
}
}

View File

@ -0,0 +1,26 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import emu.skyline.R
import kotlinx.android.synthetic.main.section_item.*
private object HeaderLayoutFactory : GenericLayoutFactory {
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.section_item, parent, false)
}
class HeaderViewItem(private val text : String) : GenericViewHolderBinder() {
override fun getLayoutFactory() : GenericLayoutFactory = HeaderLayoutFactory
override fun bind(holder : GenericViewHolder, position : Int) {
holder.text_title.text = text
}
override fun toString() = ""
}

View File

@ -1,108 +0,0 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.adapter
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import emu.skyline.R
import emu.skyline.data.BaseItem
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.log_item.*
/**
* This class is used to hold all data about a log entry
*/
internal class LogItem(val message : String, val level : String) : BaseItem() {
/**
* The log message itself is used as the search key
*/
override fun key() : String? {
return message
}
}
/**
* This adapter is used for displaying logs outputted by the application
*/
internal class LogAdapter internal constructor(val context : Context, val compact : Boolean, private val debug_level : Int, private val level_str : Array<String>) : HeaderAdapter<LogItem, BaseHeader, RecyclerView.ViewHolder>() {
private val clipboard : ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
/**
* This function adds a line to this log adapter
*/
fun add(logLine : String) {
try {
val logMeta = logLine.split("|", limit = 3)
if (logMeta[0].startsWith("1")) {
val level = logMeta[1].toInt()
if (level > debug_level) return
addItem(LogItem(logMeta[2].replace('\\', '\n'), level_str[level]))
} else {
addHeader(BaseHeader(logMeta[1]))
}
} catch (ignored : IndexOutOfBoundsException) {
} catch (ignored : NumberFormatException) {
}
}
private class ItemViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
private class HeaderViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
/**
* This function creates the view-holder of type [viewType] with the layout parent as [parent]
*/
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) : RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(context)
val view = when (ElementType.values()[viewType]) {
ElementType.Item -> inflater.inflate(if (compact) R.layout.log_item_compact else R.layout.log_item, parent, false)
ElementType.Header -> inflater.inflate(R.layout.log_item, parent, false)
}
return when (ElementType.values()[viewType]) {
ElementType.Item -> {
if (compact) {
ItemViewHolder(view)
} else {
ItemViewHolder(view)
}
}
ElementType.Header -> {
HeaderViewHolder(view)
}
}
}
/**
* This function binds the item at [position] to the supplied [holder]
*/
override fun onBindViewHolder(holder : RecyclerView.ViewHolder, position : Int) {
val item = getItem(position)
if (item is LogItem && holder is ItemViewHolder) {
holder.text_title.text = item.message
holder.text_subtitle?.text = item.level
holder.itemView.setOnClickListener {
clipboard.setPrimaryClip(ClipData.newPlainText("Log Message", item.message + " (" + item.level + ")"))
Toast.makeText(holder.itemView.context, "Copied to clipboard", Toast.LENGTH_LONG).show()
}
} else if (item is BaseHeader && holder is HeaderViewHolder) {
holder.text_title.text = item.title
}
}
}

View File

@ -0,0 +1,33 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.adapter
import android.content.ClipData
import android.content.ClipboardManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import emu.skyline.R
import kotlinx.android.synthetic.main.log_item.*
private data class LogLayoutFactory(private val compact : Boolean) : GenericLayoutFactory {
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(if (compact) R.layout.log_item_compact else R.layout.log_item, parent, false)
}
class LogViewItem(private val compact : Boolean, private val message : String, private val level : String) : GenericViewHolderBinder() {
override fun getLayoutFactory() : GenericLayoutFactory = LogLayoutFactory(compact)
override fun bind(holder : GenericViewHolder, position : Int) {
holder.text_title.text = message
holder.text_subtitle?.text = level
holder.itemView.setOnClickListener {
it.context.getSystemService(ClipboardManager::class.java).setPrimaryClip(ClipData.newPlainText("Log Message", "$message ($level)"))
Toast.makeText(it.context, "Copied to clipboard", Toast.LENGTH_LONG).show()
}
}
}

View File

@ -0,0 +1,26 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.adapter.controller
import emu.skyline.adapter.GenericViewHolder
import emu.skyline.input.ButtonGuestEvent
import emu.skyline.input.ButtonId
import emu.skyline.input.InputManager
/**
* This item is used to display a particular [button] mapping for the controller
*/
class ControllerButtonViewItem(private val controllerId : Int, val button : ButtonId, private val onClick : (item : ControllerButtonViewItem, position : Int) -> Unit) : ControllerViewItem() {
override fun bind(holder : GenericViewHolder, position : Int) {
content = button.long?.let { holder.itemView.context.getString(it) } ?: button.toString()
val guestEvent = ButtonGuestEvent(controllerId, button)
subContent = InputManager.eventMap.filter { it.value is ButtonGuestEvent && it.value == guestEvent }.keys.firstOrNull()?.toString() ?: ""
super.bind(holder, position)
holder.itemView.setOnClickListener { onClick.invoke(this, position) }
}
}

View File

@ -0,0 +1,33 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.adapter.controller
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import emu.skyline.R
import emu.skyline.adapter.GenericLayoutFactory
import emu.skyline.adapter.GenericViewHolder
import emu.skyline.adapter.GenericViewHolderBinder
import kotlinx.android.synthetic.main.controller_checkbox_item.*
private object ControllerCheckBoxLayoutFactory : GenericLayoutFactory {
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.controller_checkbox_item, parent, false)
}
class ControllerCheckBoxViewItem(var title : String, var summary : String, var checked : Boolean, private val onCheckedChange : (item : ControllerCheckBoxViewItem, position : Int) -> Unit) : GenericViewHolderBinder() {
override fun getLayoutFactory() : GenericLayoutFactory = ControllerCheckBoxLayoutFactory
override fun bind(holder : GenericViewHolder, position : Int) {
holder.text_title.text = title
holder.text_subtitle.text = summary
holder.checkbox.isChecked = checked
holder.itemView.setOnClickListener {
checked = !checked
onCheckedChange.invoke(this, position)
}
}
}

View File

@ -0,0 +1,41 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.adapter.controller
import emu.skyline.R
import emu.skyline.adapter.GenericViewHolder
import emu.skyline.input.GeneralType
import emu.skyline.input.InputManager
import emu.skyline.input.JoyConLeftController
/**
* This item is used to display general settings items regarding controller
*
* @param type The type of controller setting this item is displaying
*/
class ControllerGeneralViewItem(private val controllerId : Int, val type : GeneralType, private val onClick : (item : ControllerGeneralViewItem, position : Int) -> Unit) : ControllerViewItem() {
override fun bind(holder : GenericViewHolder, position : Int) {
val context = holder.itemView.context
val controller = InputManager.controllers[controllerId]!!
content = context.getString(type.stringRes)
subContent = when (type) {
GeneralType.PartnerJoyCon -> {
val partner = (controller as JoyConLeftController).partnerId
if (partner != null)
"${context.getString(R.string.controller)} #${partner + 1}"
else
context.getString(R.string.none)
}
GeneralType.RumbleDevice -> controller.rumbleDeviceName ?: context.getString(R.string.none)
}
super.bind(holder, position)
holder.itemView.setOnClickListener { onClick.invoke(this, position) }
}
}

View File

@ -0,0 +1,43 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.adapter.controller
import emu.skyline.R
import emu.skyline.adapter.GenericViewHolder
import emu.skyline.input.AxisGuestEvent
import emu.skyline.input.ButtonGuestEvent
import emu.skyline.input.InputManager
import emu.skyline.input.StickId
/**
* This item is used to display all information regarding a [stick] and it's mappings for the controller
*/
class ControllerStickViewItem(private val controllerId : Int, val stick : StickId, private val onClick : (item : ControllerStickViewItem, position : Int) -> Unit) : ControllerViewItem(stick.toString()) {
override fun bind(holder : GenericViewHolder, position : Int) {
val context = holder.itemView.context
val buttonGuestEvent = ButtonGuestEvent(controllerId, stick.button)
val button = InputManager.eventMap.filter { it.value is ButtonGuestEvent && it.value == buttonGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
var axisGuestEvent = AxisGuestEvent(controllerId, stick.yAxis, true)
val yAxisPlus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
axisGuestEvent = AxisGuestEvent(controllerId, stick.yAxis, false)
val yAxisMinus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
axisGuestEvent = AxisGuestEvent(controllerId, stick.xAxis, true)
val xAxisPlus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
axisGuestEvent = AxisGuestEvent(controllerId, stick.xAxis, false)
val xAxisMinus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
subContent = "${context.getString(R.string.button)}: $button\n${context.getString(R.string.up)}: $yAxisPlus\n${context.getString(R.string.down)}: $yAxisMinus\n${context.getString(R.string.left)}: $xAxisMinus\n${context.getString(R.string.right)}: $xAxisPlus"
super.bind(holder, position)
holder.itemView.setOnClickListener { onClick.invoke(this, position) }
}
}

View File

@ -0,0 +1,26 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.adapter.controller
import emu.skyline.R
import emu.skyline.adapter.GenericViewHolder
import emu.skyline.input.ControllerType
/**
* This item is used to display the [type] of the currently active controller
*/
class ControllerTypeViewItem(private val type : ControllerType, private val onClick : (item : ControllerTypeViewItem, position : Int) -> Unit) : ControllerViewItem() {
override fun bind(holder : GenericViewHolder, position : Int) {
val context = holder.itemView.context
content = context.getString(R.string.controller_type)
subContent = context.getString(type.stringRes)
super.bind(holder, position)
holder.itemView.setOnClickListener { onClick.invoke(this, position) }
}
}

View File

@ -0,0 +1,41 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.adapter.controller
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isGone
import emu.skyline.R
import emu.skyline.adapter.GenericLayoutFactory
import emu.skyline.adapter.GenericViewHolder
import emu.skyline.adapter.GenericViewHolderBinder
import kotlinx.android.synthetic.main.controller_item.*
private object ControllerLayoutFactory : GenericLayoutFactory {
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.controller_item, parent, false)
}
open class ControllerViewItem(var content : String = "", var subContent : String = "", private val onClick : (() -> Unit)? = null) : GenericViewHolderBinder() {
private var position = -1
override fun getLayoutFactory() : GenericLayoutFactory = ControllerLayoutFactory
override fun bind(holder : GenericViewHolder, position : Int) {
this.position = position
holder.text_title.apply {
isGone = content.isEmpty()
text = content
}
holder.text_subtitle.apply {
isGone = subContent.isEmpty()
text = subContent
}
onClick?.let { onClick -> holder.itemView.setOnClickListener { onClick.invoke() } }
}
fun update() = adapter?.notifyItemChanged(position)
}

View File

@ -6,8 +6,6 @@
package emu.skyline.data
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import emu.skyline.R
import emu.skyline.loader.AppEntry
import emu.skyline.loader.LoaderResult
@ -19,32 +17,27 @@ class AppItem(val meta : AppEntry) : BaseItem() {
/**
* The icon of the application
*/
val icon : Bitmap?
get() = meta.icon
val icon get() = meta.icon
/**
* The title of the application
*/
val title : String
get() = meta.name
val title get() = meta.name
/**
* The string used as the sub-title, we currently use the author
*/
val subTitle : String?
get() = meta.author
val subTitle get() = meta.author
/**
* The URI of the application's image file
*/
val uri : Uri
get() = meta.uri
val uri get() = meta.uri
/**
* The format of the application ROM as a string
*/
private val type : String
get() = meta.format.name
private val type get() = meta.format.name
val loaderResult get() = meta.loaderResult
@ -63,7 +56,5 @@ class AppItem(val meta : AppEntry) : BaseItem() {
/**
* The name and author is used as the key
*/
override fun key() : String? {
return if (meta.author != null) meta.name + " " + meta.author else meta.name
}
override fun key() = meta.name + if (meta.author != null) " ${meta.author}" else ""
}

View File

@ -0,0 +1,15 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.data
import java.io.Serializable
enum class ElementType {
Header,
Item
}
abstract class BaseElement(elementType : ElementType) : Serializable

View File

@ -0,0 +1,8 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.data
class BaseHeader(val title : String) : BaseElement(ElementType.Header)

View File

@ -5,9 +5,6 @@
package emu.skyline.data
import emu.skyline.adapter.BaseElement
import emu.skyline.adapter.ElementType
/**
* This is an abstract class that all adapter item classes inherit from
*/
@ -15,5 +12,5 @@ abstract class BaseItem : BaseElement(ElementType.Item) {
/**
* This function returns a string used for searching
*/
open fun key() : String? = null
open fun key() : String = ""
}

View File

@ -5,16 +5,20 @@
package emu.skyline.input
import android.content.Intent
import android.os.Bundle
import android.view.KeyEvent
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import emu.skyline.R
import emu.skyline.adapter.*
import emu.skyline.adapter.GenericAdapter
import emu.skyline.adapter.HeaderViewItem
import emu.skyline.adapter.controller.*
import emu.skyline.input.dialog.ButtonDialog
import emu.skyline.input.dialog.RumbleDialog
import emu.skyline.input.dialog.StickDialog
import emu.skyline.input.onscreen.OnScreenEditActivity
import kotlinx.android.synthetic.main.controller_activity.*
import kotlinx.android.synthetic.main.titlebar.*
@ -30,43 +34,54 @@ class ControllerActivity : AppCompatActivity() {
/**
* The adapter used by [controller_list] to hold all the items
*/
private val adapter = ControllerAdapter(::onControllerItemClick)
private val adapter = GenericAdapter()
/**
* This is a map between a button and it's corresponding [ControllerItem] in [adapter]
*/
val buttonMap = mutableMapOf<ButtonId, ControllerItem>()
val buttonMap = mutableMapOf<ButtonId, ControllerViewItem>()
/**
* This is a map between an axis and it's corresponding [ControllerStickItem] in [adapter]
* This is a map between an axis and it's corresponding [ControllerStickViewItem] in [adapter]
*/
val axisMap = mutableMapOf<AxisId, ControllerStickItem>()
val axisMap = mutableMapOf<AxisId, ControllerStickViewItem>()
/**
* This function updates the [adapter] based on information from [InputManager]
*/
private fun update() {
adapter.clear()
adapter.removeAllItems()
val controller = InputManager.controllers[id]!!
adapter.addItem(ControllerTypeItem(this, controller.type))
adapter.addItem(ControllerTypeViewItem(controller.type, onControllerTypeClick))
if (controller.type == ControllerType.None)
return
if (id == 0 && controller.type.firstController) {
adapter.addItem(HeaderViewItem(getString(R.string.osc)))
adapter.addItem(ControllerCheckBoxViewItem(getString(R.string.osc_enable), getString(R.string.osc_not_shown), false) { item, position ->
item.summary = getString(if (item.checked) R.string.osc_shown else R.string.osc_not_shown)
adapter.notifyItemChanged(position)
})
adapter.addItem(ControllerViewItem(content = getString(R.string.osc_edit), onClick = {
startActivity(Intent(this, OnScreenEditActivity::class.java))
}))
}
var wroteTitle = false
for (item in GeneralType.values()) {
if (item.compatibleControllers == null || item.compatibleControllers.contains(controller.type)) {
if (!wroteTitle) {
adapter.addHeader(getString(R.string.general))
adapter.addItem(HeaderViewItem(getString(R.string.general)))
wroteTitle = true
}
adapter.addItem(ControllerGeneralItem(this, item))
adapter.addItem(ControllerGeneralViewItem(id, item, onControllerGeneralClick))
}
}
@ -74,11 +89,11 @@ class ControllerActivity : AppCompatActivity() {
for (stick in controller.type.sticks) {
if (!wroteTitle) {
adapter.addHeader(getString(R.string.sticks))
adapter.addItem(HeaderViewItem(getString(R.string.sticks)))
wroteTitle = true
}
val stickItem = ControllerStickItem(this, stick)
val stickItem = ControllerStickViewItem(id, stick, onControllerStickClick)
adapter.addItem(stickItem)
buttonMap[stick.button] = stickItem
@ -98,11 +113,11 @@ class ControllerActivity : AppCompatActivity() {
for (button in controller.type.buttons.filter { it in buttonArray.second }) {
if (!wroteTitle) {
adapter.addHeader(getString(buttonArray.first))
adapter.addItem(HeaderViewItem(getString(buttonArray.first)))
wroteTitle = true
}
val buttonItem = ControllerButtonItem(this, button)
val buttonItem = ControllerButtonViewItem(id, button, onControllerButtonClick)
adapter.addItem(buttonItem)
buttonMap[button] = buttonItem
@ -113,11 +128,11 @@ class ControllerActivity : AppCompatActivity() {
for (button in controller.type.buttons.filterNot { item -> buttonArrays.any { item in it.second } }.plus(ButtonId.Menu)) {
if (!wroteTitle) {
adapter.addHeader(getString(R.string.misc_buttons))
adapter.addItem(HeaderViewItem(getString(R.string.misc_buttons)))
wroteTitle = true
}
val buttonItem = ControllerButtonItem(this, button)
val buttonItem = ControllerButtonViewItem(id, button, onControllerButtonClick)
adapter.addItem(buttonItem)
buttonMap[button] = buttonItem
@ -154,83 +169,81 @@ class ControllerActivity : AppCompatActivity() {
super.onPause()
}
private fun onControllerItemClick(item : ControllerItem) {
when (item) {
is ControllerTypeItem -> {
val controller = InputManager.controllers[id]!!
private val onControllerTypeClick = { item : ControllerTypeViewItem, _ : Int ->
val controller = InputManager.controllers[id]!!
val types = ControllerType.values().apply { if (id != 0) filter { !it.firstController } }
val typeNames = types.map { getString(it.stringRes) }.toTypedArray()
val types = ControllerType.values().apply { if (id != 0) filter { !it.firstController } }
val typeNames = types.map { getString(it.stringRes) }.toTypedArray()
MaterialAlertDialogBuilder(this)
.setTitle(item.content)
.setSingleChoiceItems(typeNames, types.indexOf(controller.type)) { dialog, typeIndex ->
val selectedType = types[typeIndex]
if (controller.type != selectedType) {
if (controller is JoyConLeftController)
controller.partnerId?.let { (InputManager.controllers[it] as JoyConRightController).partnerId = null }
else if (controller is JoyConRightController)
controller.partnerId?.let { (InputManager.controllers[it] as JoyConLeftController).partnerId = null }
InputManager.controllers[id] = when (selectedType) {
ControllerType.None -> Controller(id, ControllerType.None)
ControllerType.HandheldProController -> HandheldController(id)
ControllerType.ProController -> ProController(id)
ControllerType.JoyConLeft -> JoyConLeftController(id)
ControllerType.JoyConRight -> JoyConRightController(id)
}
update()
}
dialog.dismiss()
}
.show()
Unit
}
private val onControllerGeneralClick = { item : ControllerGeneralViewItem, _ : Int ->
when (item.type) {
GeneralType.PartnerJoyCon -> {
val controller = InputManager.controllers[id] as JoyConLeftController
val rJoyCons = InputManager.controllers.values.filter { it.type == ControllerType.JoyConRight }
val rJoyConNames = (listOf(getString(R.string.none)) + rJoyCons.map { "${getString(R.string.controller)} #${it.id + 1}" }).toTypedArray()
val partnerNameIndex = controller.partnerId?.let { partnerId ->
rJoyCons.withIndex().single { it.value.id == partnerId }.index + 1
} ?: 0
MaterialAlertDialogBuilder(this)
.setTitle(item.content)
.setSingleChoiceItems(typeNames, types.indexOf(controller.type)) { dialog, typeIndex ->
val selectedType = types[typeIndex]
if (controller.type != selectedType) {
if (controller is JoyConLeftController)
controller.partnerId?.let { (InputManager.controllers[it] as JoyConRightController).partnerId = null }
else if (controller is JoyConRightController)
controller.partnerId?.let { (InputManager.controllers[it] as JoyConLeftController).partnerId = null }
.setSingleChoiceItems(rJoyConNames, partnerNameIndex) { dialog, index ->
(InputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = null
InputManager.controllers[id] = when (selectedType) {
ControllerType.None -> Controller(id, ControllerType.None)
ControllerType.HandheldProController -> HandheldController(id)
ControllerType.ProController -> ProController(id)
ControllerType.JoyConLeft -> JoyConLeftController(id)
ControllerType.JoyConRight -> JoyConRightController(id)
}
controller.partnerId = if (index == 0) null else rJoyCons[index - 1].id
update()
}
if (controller.partnerId != null)
(InputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = controller.id
item.update()
dialog.dismiss()
}
.show()
}
is ControllerGeneralItem -> {
when (item.type) {
GeneralType.PartnerJoyCon -> {
val controller = InputManager.controllers[id] as JoyConLeftController
val rJoyCons = InputManager.controllers.values.filter { it.type == ControllerType.JoyConRight }
val rJoyConNames = (listOf(getString(R.string.none)) + rJoyCons.map { "${getString(R.string.controller)} #${it.id + 1}" }).toTypedArray()
val partnerNameIndex = controller.partnerId?.let { partnerId ->
rJoyCons.withIndex().single { it.value.id == partnerId }.index + 1
} ?: 0
MaterialAlertDialogBuilder(this)
.setTitle(item.content)
.setSingleChoiceItems(rJoyConNames, partnerNameIndex) { dialog, index ->
(InputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = null
controller.partnerId = if (index == 0) null else rJoyCons[index - 1].id
if (controller.partnerId != null)
(InputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = controller.id
item.update()
dialog.dismiss()
}
.show()
}
GeneralType.RumbleDevice -> {
RumbleDialog(item).show(supportFragmentManager, null)
}
}
}
is ControllerButtonItem -> {
ButtonDialog(item).show(supportFragmentManager, null)
}
is ControllerStickItem -> {
StickDialog(item).show(supportFragmentManager, null)
GeneralType.RumbleDevice -> {
RumbleDialog(item).show(supportFragmentManager, null)
}
}
Unit
}
private val onControllerButtonClick = { item : ControllerButtonViewItem, _ : Int ->
ButtonDialog(item).show(supportFragmentManager, null)
}
private val onControllerStickClick = { item : ControllerStickViewItem, _ : Int ->
StickDialog(item).show(supportFragmentManager, null)
}
/**

View File

@ -14,7 +14,7 @@ import android.view.animation.LinearInterpolator
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import emu.skyline.R
import emu.skyline.adapter.ControllerButtonItem
import emu.skyline.adapter.controller.ControllerButtonViewItem
import emu.skyline.input.*
import kotlinx.android.synthetic.main.button_dialog.*
import kotlin.math.abs
@ -22,9 +22,9 @@ import kotlin.math.abs
/**
* This dialog is used to set a device to map any buttons
*
* @param item This is used to hold the [ControllerButtonItem] between instances
* @param item This is used to hold the [ControllerButtonViewItem] between instances
*/
class ButtonDialog @JvmOverloads constructor(private val item : ControllerButtonItem? = null) : BottomSheetDialogFragment() {
class ButtonDialog @JvmOverloads constructor(private val item : ControllerButtonViewItem? = null, private val position : Int? = null) : BottomSheetDialogFragment() {
/**
* This inflates the layout of the dialog after initial view creation
*/

View File

@ -15,7 +15,7 @@ import android.view.animation.LinearInterpolator
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import emu.skyline.R
import emu.skyline.adapter.ControllerGeneralItem
import emu.skyline.adapter.controller.ControllerGeneralViewItem
import emu.skyline.input.ControllerActivity
import emu.skyline.input.InputManager
import kotlinx.android.synthetic.main.rumble_dialog.*
@ -23,9 +23,9 @@ import kotlinx.android.synthetic.main.rumble_dialog.*
/**
* This dialog is used to set a device to pass on any rumble/force feedback data onto
*
* @param item This is used to hold the [ControllerGeneralItem] between instances
* @param item This is used to hold the [ControllerGeneralViewItem] between instances
*/
class RumbleDialog @JvmOverloads constructor(val item : ControllerGeneralItem? = null) : BottomSheetDialogFragment() {
class RumbleDialog @JvmOverloads constructor(val item : ControllerGeneralViewItem? = null) : BottomSheetDialogFragment() {
/**
* This inflates the layout of the dialog after initial view creation
*/

View File

@ -15,7 +15,7 @@ import android.view.animation.LinearInterpolator
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import emu.skyline.R
import emu.skyline.adapter.ControllerStickItem
import emu.skyline.adapter.controller.ControllerStickViewItem
import emu.skyline.input.*
import emu.skyline.input.MotionHostEvent.Companion.axes
import kotlinx.android.synthetic.main.stick_dialog.*
@ -26,9 +26,9 @@ import kotlin.math.max
/**
* This dialog is used to set a device to map any sticks
*
* @param item This is used to hold the [ControllerStickItem] between instances
* @param item This is used to hold the [ControllerStickViewItem] between instances
*/
class StickDialog @JvmOverloads constructor(val item : ControllerStickItem? = null) : BottomSheetDialogFragment() {
class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem? = null) : BottomSheetDialogFragment() {
/**
* This enumerates all of the stages this dialog can be in
*/

View File

@ -0,0 +1,12 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.input.onscreen
import androidx.appcompat.app.AppCompatActivity
class OnScreenEditActivity : AppCompatActivity() {
}

View File

@ -10,7 +10,10 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.provider.OpenableColumns
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.Serializable
import java.util.*
@ -60,12 +63,40 @@ enum class LoaderResult(val value : Int) {
/**
* This class is used to hold an application's metadata in a serializable way
*/
class AppEntry(val name : String, val author : String?, val icon : Bitmap?, val format : RomFormat, val uri : Uri, val loaderResult : LoaderResult) : Serializable {
class AppEntry(var name : String, var author : String?, var icon : Bitmap?, var format : RomFormat, var uri : Uri, var loaderResult : LoaderResult) : Serializable {
constructor(context : Context, format : RomFormat, uri : Uri, loaderResult : LoaderResult) : this(context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex : Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
cursor.getString(nameIndex)
}!!.dropLast(format.name.length + 1), null, null, format, uri, loaderResult)
private fun writeObject(output : ObjectOutputStream) {
output.writeUTF(name)
output.writeObject(format)
output.writeUTF(uri.toString())
output.writeBoolean(author != null)
if (author != null)
output.writeUTF(author)
output.writeInt(loaderResult.value)
output.writeBoolean(icon != null)
icon?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
it.compress(Bitmap.CompressFormat.WEBP_LOSSY, 100, output)
else
it.compress(Bitmap.CompressFormat.WEBP, 100, output)
}
}
private fun readObject(input : ObjectInputStream) {
name = input.readUTF()
format = input.readObject() as RomFormat
uri = Uri.parse(input.readUTF())
if (input.readBoolean())
author = input.readUTF()
loaderResult = LoaderResult.get(input.readInt())
if (input.readBoolean())
icon = BitmapFactory.decodeStream(input)
}
}
/**

View File

@ -0,0 +1,52 @@
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/controller_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<TextView
android:id="@+id/text_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:textAppearance="?android:attr/textAppearanceListItem"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@id/text_subtitle"
app:layout_constraintEnd_toStartOf="@id/checkbox"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Title" />
<TextView
android:id="@+id/text_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginBottom="16dp"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="@android:color/tertiary_text_light"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/checkbox"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_title"
tools:text="Summary" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -8,7 +8,7 @@
android:clickable="true"
android:focusable="true"
android:orientation="vertical"
android:padding="15dp">
android:padding="16dp">
<TextView
android:id="@+id/text_title"

View File

@ -15,5 +15,6 @@
android:fastScrollEnabled="true"
android:focusedByDefault="true"
android:transcriptMode="normal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -52,9 +52,11 @@
<string name="import_keys_failed">Failed to import keys</string>
<!-- Input -->
<string name="input">Input</string>
<string name="show_osc">Show On-Screen Controls</string>
<string name="osc">On-Screen Controls</string>
<string name="osc_enable">Enable On-Screen Controls</string>
<string name="osc_not_shown">On-Screen Controls won\'t be shown</string>
<string name="osc_shown">On-Screen Controls will be shown</string>
<string name="osc_edit">Edit On-Screen Controls layout</string>
<string name="controller">Controller</string>
<string name="config_controller">Configure Controller</string>
<string name="controller_type">Controller Type</string>