From 22140defae6a6e5941cd0604ef00a9ec6af11c26 Mon Sep 17 00:00:00 2001 From: Willi Ye Date: Sat, 3 Oct 2020 11:58:34 +0200 Subject: [PATCH] Rewrite adapter to handle any layout --- app/src/main/AndroidManifest.xml | 7 + app/src/main/java/emu/skyline/LogActivity.kt | 36 ++- app/src/main/java/emu/skyline/MainActivity.kt | 108 +++++--- .../java/emu/skyline/SerializationHelper.kt | 22 ++ .../java/emu/skyline/adapter/AppAdapter.kt | 114 -------- .../java/emu/skyline/adapter/AppViewItem.kt | 73 ++++++ .../emu/skyline/adapter/ControllerAdapter.kt | 189 -------------- .../emu/skyline/adapter/GenericAdapter.kt | 116 ++++++++ .../adapter/GenericViewHolderBinder.kt | 25 ++ .../java/emu/skyline/adapter/HeaderAdapter.kt | 247 ------------------ .../emu/skyline/adapter/HeaderViewItem.kt | 26 ++ .../java/emu/skyline/adapter/LogAdapter.kt | 108 -------- .../java/emu/skyline/adapter/LogViewItem.kt | 33 +++ .../controller/ControllerButtonViewItem.kt | 26 ++ .../controller/ControllerCheckBoxViewItem.kt | 33 +++ .../controller/ControllerGeneralViewItem.kt | 41 +++ .../controller/ControllerStickViewItem.kt | 43 +++ .../controller/ControllerTypeViewItem.kt | 26 ++ .../adapter/controller/ControllerViewItem.kt | 41 +++ app/src/main/java/emu/skyline/data/AppItem.kt | 21 +- .../main/java/emu/skyline/data/BaseElement.kt | 15 ++ .../main/java/emu/skyline/data/BaseHeader.kt | 8 + .../main/java/emu/skyline/data/BaseItem.kt | 5 +- .../emu/skyline/input/ControllerActivity.kt | 169 ++++++------ .../emu/skyline/input/dialog/ButtonDialog.kt | 6 +- .../emu/skyline/input/dialog/RumbleDialog.kt | 6 +- .../emu/skyline/input/dialog/StickDialog.kt | 6 +- .../input/onscreen/OnScreenEditActivity.kt | 12 + .../main/java/emu/skyline/loader/RomFile.kt | 33 ++- .../res/layout/controller_checkbox_item.xml | 52 ++++ app/src/main/res/layout/controller_item.xml | 2 +- app/src/main/res/layout/log_activity.xml | 1 + app/src/main/res/values/strings.xml | 4 +- 33 files changed, 843 insertions(+), 811 deletions(-) create mode 100644 app/src/main/java/emu/skyline/SerializationHelper.kt delete mode 100644 app/src/main/java/emu/skyline/adapter/AppAdapter.kt create mode 100644 app/src/main/java/emu/skyline/adapter/AppViewItem.kt delete mode 100644 app/src/main/java/emu/skyline/adapter/ControllerAdapter.kt create mode 100644 app/src/main/java/emu/skyline/adapter/GenericAdapter.kt create mode 100644 app/src/main/java/emu/skyline/adapter/GenericViewHolderBinder.kt delete mode 100644 app/src/main/java/emu/skyline/adapter/HeaderAdapter.kt create mode 100644 app/src/main/java/emu/skyline/adapter/HeaderViewItem.kt delete mode 100644 app/src/main/java/emu/skyline/adapter/LogAdapter.kt create mode 100644 app/src/main/java/emu/skyline/adapter/LogViewItem.kt create mode 100644 app/src/main/java/emu/skyline/adapter/controller/ControllerButtonViewItem.kt create mode 100644 app/src/main/java/emu/skyline/adapter/controller/ControllerCheckBoxViewItem.kt create mode 100644 app/src/main/java/emu/skyline/adapter/controller/ControllerGeneralViewItem.kt create mode 100644 app/src/main/java/emu/skyline/adapter/controller/ControllerStickViewItem.kt create mode 100644 app/src/main/java/emu/skyline/adapter/controller/ControllerTypeViewItem.kt create mode 100644 app/src/main/java/emu/skyline/adapter/controller/ControllerViewItem.kt create mode 100644 app/src/main/java/emu/skyline/data/BaseElement.kt create mode 100644 app/src/main/java/emu/skyline/data/BaseHeader.kt create mode 100644 app/src/main/java/emu/skyline/input/onscreen/OnScreenEditActivity.kt create mode 100644 app/src/main/res/layout/controller_checkbox_item.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 52be86d7..dbfb038c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -59,6 +59,13 @@ android:name="android.support.PARENT_ACTIVITY" android:value="emu.skyline.SettingsActivity" /> + + + + 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) diff --git a/app/src/main/java/emu/skyline/MainActivity.kt b/app/src/main/java/emu/skyline/MainActivity.kt index 47a467da..f3341527 100644 --- a/app/src/main/java/emu/skyline/MainActivity.kt +++ b/app/src/main/java/emu/skyline/MainActivity.kt @@ -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, 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(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() + 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) diff --git a/app/src/main/java/emu/skyline/SerializationHelper.kt b/app/src/main/java/emu/skyline/SerializationHelper.kt new file mode 100644 index 00000000..5dcbd958 --- /dev/null +++ b/app/src/main/java/emu/skyline/SerializationHelper.kt @@ -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 ArrayList.serialize(file : File) { + ObjectOutputStream(file.outputStream()).use { + it.writeObject(this) + } +} + +@Suppress("UNCHECKED_CAST") +fun loadSerializedList(file : File) = ObjectInputStream(file.inputStream()).use { + it.readObject() +} as ArrayList diff --git a/app/src/main/java/emu/skyline/adapter/AppAdapter.kt b/app/src/main/java/emu/skyline/adapter/AppAdapter.kt deleted file mode 100644 index 689bc65b..00000000 --- a/app/src/main/java/emu/skyline/adapter/AppAdapter.kt +++ /dev/null @@ -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() { - 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() - } -} diff --git a/app/src/main/java/emu/skyline/adapter/AppViewItem.kt b/app/src/main/java/emu/skyline/adapter/AppViewItem.kt new file mode 100644 index 00000000..6a70514e --- /dev/null +++ b/app/src/main/java/emu/skyline/adapter/AppViewItem.kt @@ -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() +} diff --git a/app/src/main/java/emu/skyline/adapter/ControllerAdapter.kt b/app/src/main/java/emu/skyline/adapter/ControllerAdapter.kt deleted file mode 100644 index fe23b3ca..00000000 --- a/app/src/main/java/emu/skyline/adapter/ControllerAdapter.kt +++ /dev/null @@ -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() { - 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 - } - } -} diff --git a/app/src/main/java/emu/skyline/adapter/GenericAdapter.kt b/app/src/main/java/emu/skyline/adapter/GenericAdapter.kt new file mode 100644 index 00000000..9299cf3e --- /dev/null +++ b/app/src/main/java/emu/skyline/adapter/GenericAdapter.kt @@ -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(), Filterable { + var currentSearchTerm = "" + + val currentItems get() = if (currentSearchTerm.isEmpty()) allItems else filteredItems + val allItems = mutableListOf() + private var filteredItems = listOf() + + private val viewTypesMapping = mutableMapOf() + + 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() + + 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 + + notifyDataSetChanged() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/emu/skyline/adapter/GenericViewHolderBinder.kt b/app/src/main/java/emu/skyline/adapter/GenericViewHolderBinder.kt new file mode 100644 index 00000000..3bf2a2f0 --- /dev/null +++ b/app/src/main/java/emu/skyline/adapter/GenericViewHolderBinder.kt @@ -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) +} diff --git a/app/src/main/java/emu/skyline/adapter/HeaderAdapter.kt b/app/src/main/java/emu/skyline/adapter/HeaderAdapter.kt deleted file mode 100644 index 5698fd21..00000000 --- a/app/src/main/java/emu/skyline/adapter/HeaderAdapter.kt +++ /dev/null @@ -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 : RecyclerView.Adapter(), Filterable, Serializable { - /** - * This holds all the elements in an array even if they may not be visible - */ - var elementArray : ArrayList = ArrayList() - - /** - * This holds the indices of all the visible items in [elementArray] - */ - var visibleArray : ArrayList = 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 - 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) : Array { - val scoredItems : MutableList = 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() - - val keyArray = ArrayList() - 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 - - 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(val adapter : HeaderAdapter, 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 - } -} diff --git a/app/src/main/java/emu/skyline/adapter/HeaderViewItem.kt b/app/src/main/java/emu/skyline/adapter/HeaderViewItem.kt new file mode 100644 index 00000000..176645fb --- /dev/null +++ b/app/src/main/java/emu/skyline/adapter/HeaderViewItem.kt @@ -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() = "" +} diff --git a/app/src/main/java/emu/skyline/adapter/LogAdapter.kt b/app/src/main/java/emu/skyline/adapter/LogAdapter.kt deleted file mode 100644 index e845b5cf..00000000 --- a/app/src/main/java/emu/skyline/adapter/LogAdapter.kt +++ /dev/null @@ -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) : HeaderAdapter() { - 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 - } - } -} diff --git a/app/src/main/java/emu/skyline/adapter/LogViewItem.kt b/app/src/main/java/emu/skyline/adapter/LogViewItem.kt new file mode 100644 index 00000000..c5debb76 --- /dev/null +++ b/app/src/main/java/emu/skyline/adapter/LogViewItem.kt @@ -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() + } + } +} diff --git a/app/src/main/java/emu/skyline/adapter/controller/ControllerButtonViewItem.kt b/app/src/main/java/emu/skyline/adapter/controller/ControllerButtonViewItem.kt new file mode 100644 index 00000000..bbebac8f --- /dev/null +++ b/app/src/main/java/emu/skyline/adapter/controller/ControllerButtonViewItem.kt @@ -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) } + } +} diff --git a/app/src/main/java/emu/skyline/adapter/controller/ControllerCheckBoxViewItem.kt b/app/src/main/java/emu/skyline/adapter/controller/ControllerCheckBoxViewItem.kt new file mode 100644 index 00000000..3569e97d --- /dev/null +++ b/app/src/main/java/emu/skyline/adapter/controller/ControllerCheckBoxViewItem.kt @@ -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) + } + } +} diff --git a/app/src/main/java/emu/skyline/adapter/controller/ControllerGeneralViewItem.kt b/app/src/main/java/emu/skyline/adapter/controller/ControllerGeneralViewItem.kt new file mode 100644 index 00000000..bb08a074 --- /dev/null +++ b/app/src/main/java/emu/skyline/adapter/controller/ControllerGeneralViewItem.kt @@ -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) } + } +} diff --git a/app/src/main/java/emu/skyline/adapter/controller/ControllerStickViewItem.kt b/app/src/main/java/emu/skyline/adapter/controller/ControllerStickViewItem.kt new file mode 100644 index 00000000..e7f02e1d --- /dev/null +++ b/app/src/main/java/emu/skyline/adapter/controller/ControllerStickViewItem.kt @@ -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) } + } +} diff --git a/app/src/main/java/emu/skyline/adapter/controller/ControllerTypeViewItem.kt b/app/src/main/java/emu/skyline/adapter/controller/ControllerTypeViewItem.kt new file mode 100644 index 00000000..fde16fbf --- /dev/null +++ b/app/src/main/java/emu/skyline/adapter/controller/ControllerTypeViewItem.kt @@ -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) } + } +} diff --git a/app/src/main/java/emu/skyline/adapter/controller/ControllerViewItem.kt b/app/src/main/java/emu/skyline/adapter/controller/ControllerViewItem.kt new file mode 100644 index 00000000..212ba79f --- /dev/null +++ b/app/src/main/java/emu/skyline/adapter/controller/ControllerViewItem.kt @@ -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) +} diff --git a/app/src/main/java/emu/skyline/data/AppItem.kt b/app/src/main/java/emu/skyline/data/AppItem.kt index be6eb917..eead5b05 100644 --- a/app/src/main/java/emu/skyline/data/AppItem.kt +++ b/app/src/main/java/emu/skyline/data/AppItem.kt @@ -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 "" } diff --git a/app/src/main/java/emu/skyline/data/BaseElement.kt b/app/src/main/java/emu/skyline/data/BaseElement.kt new file mode 100644 index 00000000..25f6acd9 --- /dev/null +++ b/app/src/main/java/emu/skyline/data/BaseElement.kt @@ -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 diff --git a/app/src/main/java/emu/skyline/data/BaseHeader.kt b/app/src/main/java/emu/skyline/data/BaseHeader.kt new file mode 100644 index 00000000..9a8cf466 --- /dev/null +++ b/app/src/main/java/emu/skyline/data/BaseHeader.kt @@ -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) diff --git a/app/src/main/java/emu/skyline/data/BaseItem.kt b/app/src/main/java/emu/skyline/data/BaseItem.kt index 4fd82dd1..fed245df 100644 --- a/app/src/main/java/emu/skyline/data/BaseItem.kt +++ b/app/src/main/java/emu/skyline/data/BaseItem.kt @@ -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 = "" } diff --git a/app/src/main/java/emu/skyline/input/ControllerActivity.kt b/app/src/main/java/emu/skyline/input/ControllerActivity.kt index 5eb7bcf8..2b16c215 100644 --- a/app/src/main/java/emu/skyline/input/ControllerActivity.kt +++ b/app/src/main/java/emu/skyline/input/ControllerActivity.kt @@ -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() + val buttonMap = mutableMapOf() /** - * 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() + val axisMap = mutableMapOf() /** * 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) } /** diff --git a/app/src/main/java/emu/skyline/input/dialog/ButtonDialog.kt b/app/src/main/java/emu/skyline/input/dialog/ButtonDialog.kt index b0ca1130..54b0a15c 100644 --- a/app/src/main/java/emu/skyline/input/dialog/ButtonDialog.kt +++ b/app/src/main/java/emu/skyline/input/dialog/ButtonDialog.kt @@ -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 */ diff --git a/app/src/main/java/emu/skyline/input/dialog/RumbleDialog.kt b/app/src/main/java/emu/skyline/input/dialog/RumbleDialog.kt index 410ddcc1..d9464038 100644 --- a/app/src/main/java/emu/skyline/input/dialog/RumbleDialog.kt +++ b/app/src/main/java/emu/skyline/input/dialog/RumbleDialog.kt @@ -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 */ diff --git a/app/src/main/java/emu/skyline/input/dialog/StickDialog.kt b/app/src/main/java/emu/skyline/input/dialog/StickDialog.kt index ae2f6d81..d627d183 100644 --- a/app/src/main/java/emu/skyline/input/dialog/StickDialog.kt +++ b/app/src/main/java/emu/skyline/input/dialog/StickDialog.kt @@ -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 */ diff --git a/app/src/main/java/emu/skyline/input/onscreen/OnScreenEditActivity.kt b/app/src/main/java/emu/skyline/input/onscreen/OnScreenEditActivity.kt new file mode 100644 index 00000000..389cf34b --- /dev/null +++ b/app/src/main/java/emu/skyline/input/onscreen/OnScreenEditActivity.kt @@ -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() { + +} diff --git a/app/src/main/java/emu/skyline/loader/RomFile.kt b/app/src/main/java/emu/skyline/loader/RomFile.kt index 2eda6903..ff9fc790 100644 --- a/app/src/main/java/emu/skyline/loader/RomFile.kt +++ b/app/src/main/java/emu/skyline/loader/RomFile.kt @@ -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) + } } /** diff --git a/app/src/main/res/layout/controller_checkbox_item.xml b/app/src/main/res/layout/controller_checkbox_item.xml new file mode 100644 index 00000000..ecff3d0c --- /dev/null +++ b/app/src/main/res/layout/controller_checkbox_item.xml @@ -0,0 +1,52 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/controller_item.xml b/app/src/main/res/layout/controller_item.xml index 1abb8dc6..47ffb3a6 100644 --- a/app/src/main/res/layout/controller_item.xml +++ b/app/src/main/res/layout/controller_item.xml @@ -8,7 +8,7 @@ android:clickable="true" android:focusable="true" android:orientation="vertical" - android:padding="15dp"> + android:padding="16dp"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d34e3526..7c775bb3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -52,9 +52,11 @@ Failed to import keys Input - Show On-Screen Controls + On-Screen Controls + Enable On-Screen Controls On-Screen Controls won\'t be shown On-Screen Controls will be shown + Edit On-Screen Controls layout Controller Configure Controller Controller Type