Switch to viewbinding and di with hilt

This commit is contained in:
Willi Ye 2021-01-31 22:11:26 +01:00 committed by ◱ Mark
parent ff1b9cb510
commit f5d5caf939
41 changed files with 716 additions and 580 deletions

View File

@ -110,6 +110,10 @@
</indentOptions> </indentOptions>
</codeStyleSettings> </codeStyleSettings>
<codeStyleSettings language="XML"> <codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement> <arrangement>
<rules> <rules>
<section> <section>

View File

@ -1,6 +1,9 @@
apply plugin: 'com.android.application' plugins {
apply plugin: 'kotlin-android' id 'com.android.application'
apply plugin: 'kotlin-android-extensions' id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android { android {
compileSdkVersion 30 compileSdkVersion 30
@ -51,14 +54,7 @@ android {
} }
} }
buildFeatures { buildFeatures {
prefab true viewBinding true
viewBinding false
}
/* Android Extensions */
androidExtensions {
/* TODO: Remove this after migrating to View Bindings */
experimental = true
} }
/* Linting */ /* Linting */
@ -79,6 +75,10 @@ android {
aaptOptions { aaptOptions {
ignoreAssetsPattern "*.md" ignoreAssetsPattern "*.md"
} }
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
} }
dependencies { dependencies {
@ -99,6 +99,8 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation 'androidx.fragment:fragment-ktx:1.2.5' implementation 'androidx.fragment:fragment-ktx:1.2.5'
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
/* Kotlin */ /* Kotlin */
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"

View File

@ -20,8 +20,8 @@ import androidx.core.graphics.drawable.toBitmap
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import emu.skyline.data.AppItem import emu.skyline.data.AppItem
import emu.skyline.databinding.AppDialogBinding
import emu.skyline.loader.LoaderResult import emu.skyline.loader.LoaderResult
import kotlinx.android.synthetic.main.app_dialog.*
/** /**
* This dialog is used to show extra game metadata and provide extra options such as pinning the game to the home screen * This dialog is used to show extra game metadata and provide extra options such as pinning the game to the home screen
@ -41,19 +41,15 @@ class AppDialog : BottomSheetDialogFragment() {
} }
} }
private lateinit var item : AppItem private lateinit var binding : AppDialogBinding
private val item by lazy { requireArguments().getSerializable("item") as AppItem }
/** /**
* This inflates the layout of the dialog after initial view creation * This inflates the layout of the dialog after initial view creation
*/ */
override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) : View? { override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) : View? {
return requireActivity().layoutInflater.inflate(R.layout.app_dialog, container) return AppDialogBinding.inflate(inflater).also { binding = it }.root
}
override fun onCreate(savedInstanceState : Bundle?) {
super.onCreate(savedInstanceState)
item = requireArguments().getSerializable("item") as AppItem
} }
/** /**
@ -82,19 +78,19 @@ class AppDialog : BottomSheetDialogFragment() {
val missingIcon = ContextCompat.getDrawable(requireActivity(), R.drawable.default_icon)!!.toBitmap(256, 256) val missingIcon = ContextCompat.getDrawable(requireActivity(), R.drawable.default_icon)!!.toBitmap(256, 256)
game_icon.setImageBitmap(item.icon ?: missingIcon) binding.gameIcon.setImageBitmap(item.icon ?: missingIcon)
game_title.text = item.title binding.gameTitle.text = item.title
game_subtitle.text = item.subTitle ?: item.loaderResultString(requireContext()) binding.gameSubtitle.text = item.subTitle ?: item.loaderResultString(requireContext())
game_play.isEnabled = item.loaderResult == LoaderResult.Success binding.gamePlay.isEnabled = item.loaderResult == LoaderResult.Success
game_play.setOnClickListener { binding.gamePlay.setOnClickListener {
startActivity(Intent(activity, EmulationActivity::class.java).apply { data = item.uri }) startActivity(Intent(activity, EmulationActivity::class.java).apply { data = item.uri })
} }
val shortcutManager = requireActivity().getSystemService(ShortcutManager::class.java) val shortcutManager = requireActivity().getSystemService(ShortcutManager::class.java)
game_pin.isEnabled = shortcutManager.isRequestPinShortcutSupported binding.gamePin.isEnabled = shortcutManager.isRequestPinShortcutSupported
game_pin.setOnClickListener { binding.gamePin.setOnClickListener {
val info = ShortcutInfo.Builder(context, item.title) val info = ShortcutInfo.Builder(context, item.title)
info.setShortLabel(item.title) info.setShortLabel(item.title)
info.setActivity(ComponentName(requireContext(), EmulationActivity::class.java)) info.setActivity(ComponentName(requireContext(), EmulationActivity::class.java))

View File

@ -17,13 +17,16 @@ import android.view.*
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import dagger.hilt.android.AndroidEntryPoint
import emu.skyline.databinding.EmuActivityBinding
import emu.skyline.input.* import emu.skyline.input.*
import emu.skyline.loader.getRomFormat import emu.skyline.loader.getRomFormat
import emu.skyline.utils.Settings import emu.skyline.utils.Settings
import kotlinx.android.synthetic.main.emu_activity.*
import java.io.File import java.io.File
import javax.inject.Inject
import kotlin.math.abs import kotlin.math.abs
@AndroidEntryPoint
class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTouchListener { class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTouchListener {
companion object { companion object {
private val Tag = EmulationActivity::class.java.simpleName private val Tag = EmulationActivity::class.java.simpleName
@ -33,8 +36,10 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
System.loadLibrary("skyline") // libskyline.so System.loadLibrary("skyline") // libskyline.so
} }
private val binding by lazy { EmuActivityBinding.inflate(layoutInflater) }
/** /**
* A map of [Vibrator]s that correspond to [InputManager.controllers] * A map of [Vibrator]s that correspond to [inputManager.controllers]
*/ */
private var vibrators = HashMap<Int, Vibrator>() private var vibrators = HashMap<Int, Vibrator>()
@ -51,6 +56,9 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
private val settings by lazy { Settings(this) } private val settings by lazy { Settings(this) }
@Inject
lateinit var inputManager : InputManager
/** /**
* This is the entry point into the emulation code for libskyline * This is the entry point into the emulation code for libskyline
* *
@ -131,7 +139,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
*/ */
@Suppress("unused") @Suppress("unused")
private fun initializeControllers() { private fun initializeControllers() {
for (controller in InputManager.controllers.values) { for (controller in inputManager.controllers.values) {
if (controller.type != ControllerType.None) { if (controller.type != ControllerType.None) {
val type = when (controller.type) { val type = when (controller.type) {
ControllerType.None -> throw IllegalArgumentException() ControllerType.None -> throw IllegalArgumentException()
@ -177,11 +185,11 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
/** /**
* This makes the window fullscreen, sets up the performance statistics and finally calls [executeApplication] for executing the application * This makes the window fullscreen, sets up the performance statistics and finally calls [executeApplication] for executing the application
*/ */
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n", "ClickableViewAccessibility")
override fun onCreate(savedInstanceState : Bundle?) { override fun onCreate(savedInstanceState : Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.emu_activity) setContentView(binding.root)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.hide(WindowInsets.Type.navigationBars() or WindowInsets.Type.systemBars() or WindowInsets.Type.systemGestures() or WindowInsets.Type.statusBars()) window.insetsController?.hide(WindowInsets.Type.navigationBars() or WindowInsets.Type.systemBars() or WindowInsets.Type.systemGestures() or WindowInsets.Type.statusBars())
@ -196,32 +204,36 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
or View.SYSTEM_UI_FLAG_FULLSCREEN) or View.SYSTEM_UI_FLAG_FULLSCREEN)
} }
game_view.holder.addCallback(this) binding.gameView.holder.addCallback(this)
if (settings.perfStats) { if (settings.perfStats) {
perf_stats.postDelayed(object : Runnable { binding.perfStats.apply {
override fun run() { postDelayed(object : Runnable {
updatePerformanceStatistics() override fun run() {
perf_stats.text = "$fps FPS\n${frametime}ms" updatePerformanceStatistics()
perf_stats.postDelayed(this, 250) text = "$fps FPS\n${frametime}ms"
} postDelayed(this, 250)
}, 250) }
}, 250)
}
} }
@Suppress("DEPRECATION") val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) display!! else windowManager.defaultDisplay @Suppress("DEPRECATION") val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) display!! else windowManager.defaultDisplay
display?.supportedModes?.maxByOrNull { it.refreshRate + (it.physicalHeight * it.physicalWidth) }?.let { window.attributes.preferredDisplayModeId = it.modeId } display?.supportedModes?.maxByOrNull { it.refreshRate + (it.physicalHeight * it.physicalWidth) }?.let { window.attributes.preferredDisplayModeId = it.modeId }
game_view.setOnTouchListener(this) binding.gameView.setOnTouchListener(this)
// Hide on screen controls when first controller is not set // Hide on screen controls when first controller is not set
on_screen_controller_view.isGone = InputManager.controllers[0]!!.type == ControllerType.None || !settings.onScreenControl binding.onScreenControllerView.apply {
on_screen_controller_view.setOnButtonStateChangedListener(::onButtonStateChanged) isGone = inputManager.controllers[0]!!.type == ControllerType.None || !settings.onScreenControl
on_screen_controller_view.setOnStickStateChangedListener(::onStickStateChanged) setOnButtonStateChangedListener(::onButtonStateChanged)
on_screen_controller_view.recenterSticks = settings.onScreenControlRecenterSticks setOnStickStateChangedListener(::onStickStateChanged)
recenterSticks = settings.onScreenControlRecenterSticks
}
on_screen_controller_toggle.isGone = on_screen_controller_view.isGone binding.onScreenControllerToggle.apply {
on_screen_controller_toggle.setOnClickListener { isGone = binding.onScreenControllerView.isGone
on_screen_controller_view.isInvisible = !on_screen_controller_view.isInvisible setOnClickListener { binding.onScreenControllerView.isInvisible = !binding.onScreenControllerView.isInvisible }
} }
executeApplication(intent.data!!) executeApplication(intent.data!!)
@ -291,7 +303,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
else -> return super.dispatchKeyEvent(event) else -> return super.dispatchKeyEvent(event)
} }
return when (val guestEvent = InputManager.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) { return when (val guestEvent = inputManager.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) {
is ButtonGuestEvent -> { is ButtonGuestEvent -> {
if (guestEvent.button != ButtonId.Menu) if (guestEvent.button != ButtonId.Menu)
setButtonState(guestEvent.id, guestEvent.button.value(), action.state) setButtonState(guestEvent.id, guestEvent.button.value(), action.state)
@ -333,9 +345,9 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
var polarity = value >= 0 var polarity = value >= 0
val guestEvent = MotionHostEvent(event.device.descriptor, axis, polarity).let { hostEvent -> val guestEvent = MotionHostEvent(event.device.descriptor, axis, polarity).let { hostEvent ->
InputManager.eventMap[hostEvent] ?: if (value == 0f) { inputManager.eventMap[hostEvent] ?: if (value == 0f) {
polarity = false polarity = false
InputManager.eventMap[hostEvent.copy(polarity = false)] inputManager.eventMap[hostEvent.copy(polarity = false)]
} else { } else {
null null
} }
@ -413,7 +425,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
val vibrator = if (vibrators[index] != null) { val vibrator = if (vibrators[index] != null) {
vibrators[index]!! vibrators[index]!!
} else { } else {
InputManager.controllers[index]!!.rumbleDeviceDescriptor?.let { inputManager.controllers[index]!!.rumbleDeviceDescriptor?.let {
if (it == "builtin") { if (it == "builtin") {
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
vibrators[index] = vibrator vibrators[index] = vibrator
@ -421,7 +433,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
} else { } else {
for (id in InputDevice.getDeviceIds()) { for (id in InputDevice.getDeviceIds()) {
val device = InputDevice.getDevice(id) val device = InputDevice.getDevice(id)
if (device.descriptor == InputManager.controllers[index]!!.rumbleDeviceDescriptor) { if (device.descriptor == inputManager.controllers[index]!!.rumbleDeviceDescriptor) {
vibrators[index] = device.vibrator vibrators[index] = device.vibrator
device.vibrator device.vibrator
} }

View File

@ -20,9 +20,8 @@ import com.google.android.material.snackbar.Snackbar
import emu.skyline.adapter.GenericAdapter import emu.skyline.adapter.GenericAdapter
import emu.skyline.adapter.HeaderViewItem import emu.skyline.adapter.HeaderViewItem
import emu.skyline.adapter.LogViewItem import emu.skyline.adapter.LogViewItem
import emu.skyline.databinding.LogActivityBinding
import emu.skyline.utils.Settings import emu.skyline.utils.Settings
import kotlinx.android.synthetic.main.log_activity.*
import kotlinx.android.synthetic.main.titlebar.*
import org.json.JSONObject import org.json.JSONObject
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
@ -31,25 +30,21 @@ import java.net.URL
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
class LogActivity : AppCompatActivity() { class LogActivity : AppCompatActivity() {
private val binding by lazy { LogActivityBinding.inflate(layoutInflater) }
/** /**
* The log file is used to read log entries from or to clear all entries * The log file is used to read log entries from or to clear all entries
*/ */
private lateinit var logFile : File private lateinit var logFile : File
/**
* The adapter used for adding elements from the log to [log_list]
*/
private val adapter = GenericAdapter() private val adapter = GenericAdapter()
/**
* This initializes [toolbar] and fills [log_list] with data from the logs
*/
override fun onCreate(savedInstanceState : Bundle?) { override fun onCreate(savedInstanceState : Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.log_activity) setContentView(binding.root)
setSupportActionBar(toolbar) setSupportActionBar(binding.titlebar.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
val settings = Settings(this) val settings = Settings(this)
@ -58,10 +53,9 @@ class LogActivity : AppCompatActivity() {
val logLevel = settings.logLevel.toInt() val logLevel = settings.logLevel.toInt()
val logLevels = resources.getStringArray(R.array.log_level) val logLevels = resources.getStringArray(R.array.log_level)
log_list.adapter = adapter binding.logList.adapter = adapter
if (!compact) if (!compact) binding.logList.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
log_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
try { try {
logFile = File(applicationContext.filesDir.canonicalPath + "/skyline.log") logFile = File(applicationContext.filesDir.canonicalPath + "/skyline.log")

View File

@ -31,12 +31,14 @@ import emu.skyline.adapter.LayoutType
import emu.skyline.data.AppItem import emu.skyline.data.AppItem
import emu.skyline.data.DataItem import emu.skyline.data.DataItem
import emu.skyline.data.HeaderItem import emu.skyline.data.HeaderItem
import emu.skyline.databinding.MainActivityBinding
import emu.skyline.loader.LoaderResult import emu.skyline.loader.LoaderResult
import emu.skyline.utils.Settings import emu.skyline.utils.Settings
import kotlinx.android.synthetic.main.main_activity.*
import kotlin.math.ceil import kotlin.math.ceil
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private val binding by lazy { MainActivityBinding.inflate(layoutInflater) }
private val settings by lazy { Settings(this) } private val settings by lazy { Settings(this) }
private val adapter = GenericAdapter() private val adapter = GenericAdapter()
@ -60,24 +62,23 @@ class MainActivity : AppCompatActivity() {
) )
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(binding.root)
setContentView(R.layout.main_activity)
PreferenceManager.setDefaultValues(this, R.xml.preferences, false) PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
setupAppList() setupAppList()
swipe_refresh_layout.apply { binding.swipeRefreshLayout.apply {
setProgressBackgroundColorSchemeColor(obtainStyledAttributes(intArrayOf(R.attr.colorPrimary)).use { it.getColor(0, Color.BLACK) }) setProgressBackgroundColorSchemeColor(obtainStyledAttributes(intArrayOf(R.attr.colorPrimary)).use { it.getColor(0, Color.BLACK) })
setColorSchemeColors(obtainStyledAttributes(intArrayOf(R.attr.colorAccent)).use { it.getColor(0, Color.BLACK) }) setColorSchemeColors(obtainStyledAttributes(intArrayOf(R.attr.colorAccent)).use { it.getColor(0, Color.BLACK) })
post { setDistanceToTriggerSync(swipe_refresh_layout.height / 3) } post { setDistanceToTriggerSync(binding.swipeRefreshLayout.height / 3) }
setOnRefreshListener { loadRoms(false) } setOnRefreshListener { loadRoms(false) }
} }
viewModel.state.observe(owner = this, onChanged = ::handleState) viewModel.stateData.observe(owner = this, onChanged = ::handleState)
loadRoms(!settings.refreshRequired) loadRoms(!settings.refreshRequired)
search_bar.apply { binding.searchBar.apply {
setLogIconListener { startActivity(Intent(context, LogActivity::class.java)) } setLogIconListener { startActivity(Intent(context, LogActivity::class.java)) }
setSettingsIconListener { startActivityForResult(Intent(context, SettingsActivity::class.java), 3) } setSettingsIconListener { startActivityForResult(Intent(context, SettingsActivity::class.java), 3) }
setRefreshIconListener { loadRoms(false) } setRefreshIconListener { loadRoms(false) }
@ -90,7 +91,7 @@ class MainActivity : AppCompatActivity() {
} }
} }
window.decorView.findViewById<View>(android.R.id.content).viewTreeObserver.addOnTouchModeChangeListener { isInTouchMode -> window.decorView.findViewById<View>(android.R.id.content).viewTreeObserver.addOnTouchModeChangeListener { isInTouchMode ->
search_bar.refreshIconVisible = !isInTouchMode binding.searchBar.refreshIconVisible = !isInTouchMode
} }
} }
@ -118,11 +119,13 @@ class MainActivity : AppCompatActivity() {
} }
private fun setAppListDecoration() { private fun setAppListDecoration() {
while (app_list.itemDecorationCount > 0) app_list.removeItemDecorationAt(0) binding.appList.apply {
when (layoutType) { while (itemDecorationCount > 0) removeItemDecorationAt(0)
LayoutType.List -> app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL)) when (layoutType) {
LayoutType.List -> addItemDecoration(DividerItemDecoration(context, RecyclerView.VERTICAL))
LayoutType.Grid, LayoutType.GridCompact -> app_list.addItemDecoration(GridSpacingItemDecoration()) LayoutType.Grid, LayoutType.GridCompact -> addItemDecoration(GridSpacingItemDecoration())
}
} }
} }
@ -132,7 +135,7 @@ class MainActivity : AppCompatActivity() {
private inner class CustomLayoutManager(gridSpan : Int) : GridLayoutManager(this, gridSpan) { private inner class CustomLayoutManager(gridSpan : Int) : GridLayoutManager(this, gridSpan) {
init { init {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position : Int) = if (layoutType == LayoutType.List || adapter.currentItems[position] is HeaderViewItem) gridSpan else 1 override fun getSpanSize(position : Int) = if (layoutType == LayoutType.List || adapter.currentItems[position].fullSpan) gridSpan else 1
} }
} }
@ -141,23 +144,24 @@ class MainActivity : AppCompatActivity() {
when (focusDirection) { when (focusDirection) {
View.FOCUS_DOWN -> { View.FOCUS_DOWN -> {
findContainingItemView(focused)?.let { focusedChild -> findContainingItemView(focused)?.let { focusedChild ->
val current = app_list.indexOfChild(focusedChild) val current = binding.appList.indexOfChild(focusedChild)
val currentSpanIndex = (focusedChild.layoutParams as LayoutParams).spanIndex val currentSpanIndex = (focusedChild.layoutParams as LayoutParams).spanIndex
for (i in current + 1 until app_list.size) { for (i in current + 1 until binding.appList.size) {
val candidate = getChildAt(i)!! val candidate = getChildAt(i)!!
// Return candidate when span index matches // Return candidate when span index matches
if (currentSpanIndex == (candidate.layoutParams as LayoutParams).spanIndex) return candidate if (currentSpanIndex == (candidate.layoutParams as LayoutParams).spanIndex) return candidate
} }
if (nextFocus == null) { if (nextFocus == null) {
app_bar_layout.setExpanded(false) // End of list, hide app bar, so bottom row is fully visible binding.appBarLayout.setExpanded(false) // End of list, hide app bar, so bottom row is fully visible
app_list.smoothScrollToPosition(adapter.itemCount) binding.appList.smoothScrollToPosition(adapter.itemCount)
} }
} }
} }
View.FOCUS_UP -> { View.FOCUS_UP -> {
if (nextFocus?.isFocusable != true) { if (nextFocus?.isFocusable != true) {
search_bar.requestFocus() binding.searchBar.requestFocus()
app_bar_layout.setExpanded(true) binding.appBarLayout.setExpanded(true)
return null return null
} }
} }
@ -167,13 +171,13 @@ class MainActivity : AppCompatActivity() {
} }
private fun setupAppList() { private fun setupAppList() {
app_list.adapter = adapter binding.appList.adapter = adapter
val itemWidth = 225 val itemWidth = 225
val metrics = resources.displayMetrics val metrics = resources.displayMetrics
val gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt() val gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt()
app_list.layoutManager = CustomLayoutManager(gridSpan) binding.appList.layoutManager = CustomLayoutManager(gridSpan)
setAppListDecoration() setAppListDecoration()
if (settings.searchLocation.isEmpty()) { if (settings.searchLocation.isEmpty()) {
@ -186,18 +190,18 @@ class MainActivity : AppCompatActivity() {
private fun handleState(state : MainState) = when (state) { private fun handleState(state : MainState) = when (state) {
MainState.Loading -> { MainState.Loading -> {
search_bar.animateRefreshIcon() binding.searchBar.animateRefreshIcon()
swipe_refresh_layout.isRefreshing = true binding.swipeRefreshLayout.isRefreshing = true
} }
is MainState.Loaded -> { is MainState.Loaded -> {
swipe_refresh_layout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
populateAdapter(state.items) populateAdapter(state.items)
} }
is MainState.Error -> Snackbar.make(findViewById(android.R.id.content), getString(R.string.error) + ": ${state.ex.localizedMessage}", Snackbar.LENGTH_SHORT).show() is MainState.Error -> Snackbar.make(findViewById(android.R.id.content), getString(R.string.error) + ": ${state.ex.localizedMessage}", Snackbar.LENGTH_SHORT).show()
} }
private fun selectStartGame(appItem : AppItem) { private fun selectStartGame(appItem : AppItem) {
if (swipe_refresh_layout.isRefreshing) return if (binding.swipeRefreshLayout.isRefreshing) return
if (settings.selectAction) if (settings.selectAction)
AppDialog.newInstance(appItem).show(supportFragmentManager, "game") AppDialog.newInstance(appItem).show(supportFragmentManager, "game")
@ -206,7 +210,7 @@ class MainActivity : AppCompatActivity() {
} }
private fun selectShowGameDialog(appItem : AppItem) { private fun selectShowGameDialog(appItem : AppItem) {
if (swipe_refresh_layout.isRefreshing) return if (binding.swipeRefreshLayout.isRefreshing) return
AppDialog.newInstance(appItem).show(supportFragmentManager, "game") AppDialog.newInstance(appItem).show(supportFragmentManager, "game")
} }
@ -281,7 +285,7 @@ class MainActivity : AppCompatActivity() {
} }
override fun onBackPressed() { override fun onBackPressed() {
search_bar.apply { binding.searchBar.apply {
if (hasFocus() && text.isNotEmpty()) { if (hasFocus() && text.isNotEmpty()) {
text = "" text = ""
clearFocus() clearFocus()

View File

@ -31,8 +31,11 @@ class MainViewModel : ViewModel() {
private val TAG = MainViewModel::class.java.simpleName private val TAG = MainViewModel::class.java.simpleName
} }
private val mutableState = MutableLiveData<MainState>() private var state
val state : LiveData<MainState> = mutableState get() = _stateData.value
set(value) = _stateData.postValue(value)
private val _stateData = MutableLiveData<MainState>()
val stateData : LiveData<MainState> = _stateData
var searchBarAnimated = false var searchBarAnimated = false
@ -66,16 +69,15 @@ class MainViewModel : ViewModel() {
* @param loadFromFile 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
*/ */
fun loadRoms(context : Context, loadFromFile : Boolean, searchLocation : Uri) { fun loadRoms(context : Context, loadFromFile : Boolean, searchLocation : Uri) {
if (mutableState.value == MainState.Loading) return if (state == MainState.Loading) return
state = MainState.Loading
mutableState.postValue(MainState.Loading)
val romsFile = File(context.filesDir.canonicalPath + "/roms.bin") val romsFile = File(context.filesDir.canonicalPath + "/roms.bin")
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
if (loadFromFile) { if (loadFromFile) {
try { try {
mutableState.postValue(MainState.Loaded(loadSerializedList(romsFile))) state = MainState.Loaded(loadSerializedList(romsFile))
return@launch return@launch
} catch (e : Exception) { } catch (e : Exception) {
Log.w(TAG, "Ran into exception while loading: ${e.message}") Log.w(TAG, "Ran into exception while loading: ${e.message}")
@ -98,9 +100,9 @@ class MainViewModel : ViewModel() {
Log.w(TAG, "Ran into exception while saving: ${e.message}") Log.w(TAG, "Ran into exception while saving: ${e.message}")
} }
mutableState.postValue(MainState.Loaded(romElements)) state = MainState.Loaded(romElements)
} catch (e : Exception) { } catch (e : Exception) {
mutableState.postValue(MainState.Error(e)) state = MainState.Error(e)
} }
} }
} }

View File

@ -11,11 +11,13 @@ import android.view.KeyEvent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceGroup import androidx.preference.PreferenceGroup
import emu.skyline.databinding.SettingsActivityBinding
import emu.skyline.preference.ActivityResultDelegate import emu.skyline.preference.ActivityResultDelegate
import emu.skyline.preference.DocumentActivity import emu.skyline.preference.DocumentActivity
import kotlinx.android.synthetic.main.titlebar.*
class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity() {
val binding by lazy { SettingsActivityBinding.inflate(layoutInflater) }
/** /**
* This is the instance of [PreferenceFragment] that is shown inside [R.id.settings] * This is the instance of [PreferenceFragment] that is shown inside [R.id.settings]
*/ */
@ -27,9 +29,9 @@ class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState : Bundle?) { override fun onCreate(savedInstanceState : Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.settings_activity) setContentView(binding.root)
setSupportActionBar(toolbar) setSupportActionBar(binding.titlebar.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportFragmentManager supportFragmentManager

View File

@ -6,14 +6,7 @@
package emu.skyline package emu.skyline
import android.app.Application import android.app.Application
import emu.skyline.input.InputManager import dagger.hilt.android.HiltAndroidApp
/** @HiltAndroidApp
* Custom application class to initialize [InputManager] class SkylineApplication : Application()
*/
class SkylineApplication : Application() {
override fun onCreate() {
super.onCreate()
InputManager.init(applicationContext)
}
}

View File

@ -10,42 +10,88 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.widget.ImageView import android.widget.ImageView
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView
import androidx.viewbinding.ViewBinding
import emu.skyline.R import emu.skyline.R
import emu.skyline.data.AppItem import emu.skyline.data.AppItem
import kotlinx.android.synthetic.main.app_item_grid_compact.* import emu.skyline.databinding.AppItemGridBinding
import emu.skyline.databinding.AppItemGridCompactBinding
import emu.skyline.databinding.AppItemLinearBinding
/** sealed class LayoutType(val builder : (parent : ViewGroup) -> ViewBinding) {
* This enumerates the type of layouts the menu can be in object List : LayoutType({ ListBinding(it) })
*/ object Grid : LayoutType({ GridBinding(it) })
enum class LayoutType(val layoutRes : Int) { object GridCompact : LayoutType({ GridCompatBinding(it) })
List(R.layout.app_item_linear),
Grid(R.layout.app_item_grid), companion object {
GridCompact(R.layout.app_item_grid_compact) fun values() = arrayListOf(List, Grid, GridCompact)
}
}
data class LayoutBindingFactory(private val layoutType : LayoutType) : ViewBindingFactory {
override fun createBinding(parent : ViewGroup) = layoutType.builder(parent)
}
interface LayoutBinding<V : ViewBinding> : ViewBinding {
val binding : V
override fun getRoot() = binding.root
val textTitle : TextView
val textSubtitle : TextView
val icon : ImageView
}
class ListBinding(parent : ViewGroup) : LayoutBinding<AppItemLinearBinding> {
override val binding = AppItemLinearBinding.inflate(parent.inflater(), parent, false)
override val textTitle = binding.textTitle
override val textSubtitle = binding.textSubtitle
override val icon = binding.icon
}
class GridBinding(parent : ViewGroup) : LayoutBinding<AppItemGridBinding> {
override val binding = AppItemGridBinding.inflate(parent.inflater(), parent, false)
override val textTitle = binding.textTitle
override val textSubtitle = binding.textSubtitle
override val icon = binding.icon
}
class GridCompatBinding(parent : ViewGroup) : LayoutBinding<AppItemGridCompactBinding> {
override val binding = AppItemGridCompactBinding.inflate(parent.inflater(), parent, false)
override val textTitle = binding.textTitle
override val textSubtitle = binding.textSubtitle
override val icon = binding.icon
} }
private typealias InteractionFunction = (appItem : AppItem) -> Unit private typealias InteractionFunction = (appItem : AppItem) -> Unit
private data class AppLayoutFactory(private val layoutType : LayoutType) : GenericLayoutFactory { class AppViewItem(var layoutType : LayoutType, private val item : AppItem, private val missingIcon : Bitmap, private val onClick : InteractionFunction, private val onLongClick : InteractionFunction) : GenericListItem<LayoutBinding<*>>() {
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(layoutType.layoutRes, parent, false) override fun getViewBindingFactory() = LayoutBindingFactory(layoutType)
}
class AppViewItem(var layoutType : LayoutType, private val item : AppItem, private val missingIcon : Bitmap, private val onClick : InteractionFunction, private val onLongClick : InteractionFunction) : GenericListItem() { override fun bind(holder : GenericViewHolder<LayoutBinding<*>>, position : Int) {
override fun getLayoutFactory() : GenericLayoutFactory = AppLayoutFactory(layoutType) holder.binding.textTitle.text = item.title
holder.binding.textSubtitle.text = item.subTitle ?: item.loaderResultString(holder.binding.root.context)
override fun bind(holder : GenericViewHolder, position : Int) { holder.binding.icon.setImageBitmap(item.icon ?: missingIcon)
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) { if (layoutType == LayoutType.List) {
holder.icon.setOnClickListener { showIconDialog(holder.icon.context, item) } holder.binding.icon.setOnClickListener { showIconDialog(it.context, item) }
} }
holder.itemView.findViewById<View>(R.id.item_click_layout).apply { holder.itemView.findViewById<View>(R.id.item_click_layout).apply {
@ -68,7 +114,7 @@ class AppViewItem(var layoutType : LayoutType, private val item : AppItem, priva
override fun key() = item.key() override fun key() = item.key()
override fun areItemsTheSame(other : GenericListItem) = key() == other.key() override fun areItemsTheSame(other : GenericListItem<LayoutBinding<*>>) = key() == other.key()
override fun areContentsTheSame(other : GenericListItem) = other is AppViewItem && layoutType == other.layoutType && item == other.item override fun areContentsTheSame(other : GenericListItem<LayoutBinding<*>>) = other is AppViewItem && layoutType == other.layoutType && item == other.item
} }

View File

@ -5,39 +5,41 @@
package emu.skyline.adapter package emu.skyline.adapter
import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Filter import android.widget.Filter
import android.widget.Filterable import android.widget.Filterable
import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import info.debatty.java.stringsimilarity.Cosine import info.debatty.java.stringsimilarity.Cosine
import info.debatty.java.stringsimilarity.JaroWinkler import info.debatty.java.stringsimilarity.JaroWinkler
import java.util.* import java.util.*
/** /**
* Can handle any view types with [GenericListItem] implemented, [GenericListItem] are differentiated by the return value of [GenericListItem.getLayoutFactory] * Can handle any view types with [GenericListItem] implemented, [GenericListItem] are differentiated by the return value of [GenericListItem.getViewBindingFactory]
*/ */
class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable { class GenericAdapter : RecyclerView.Adapter<GenericViewHolder<ViewBinding>>(), Filterable {
companion object { companion object {
private val DIFFER = object : DiffUtil.ItemCallback<GenericListItem>() { private val DIFFER = object : DiffUtil.ItemCallback<GenericListItem<ViewBinding>>() {
override fun areItemsTheSame(oldItem : GenericListItem, newItem : GenericListItem) = oldItem.areItemsTheSame(newItem) override fun areItemsTheSame(oldItem : GenericListItem<ViewBinding>, newItem : GenericListItem<ViewBinding>) = oldItem.areItemsTheSame(newItem)
override fun areContentsTheSame(oldItem : GenericListItem, newItem : GenericListItem) = oldItem.areContentsTheSame(newItem) override fun areContentsTheSame(oldItem : GenericListItem<ViewBinding>, newItem : GenericListItem<ViewBinding>) = oldItem.areContentsTheSame(newItem)
} }
} }
private val asyncListDiffer = AsyncListDiffer(this, DIFFER) private val asyncListDiffer = AsyncListDiffer(this, DIFFER)
private val allItems = mutableListOf<GenericListItem>() private val allItems = mutableListOf<GenericListItem<out ViewBinding>>()
val currentItems : List<GenericListItem> get() = asyncListDiffer.currentList val currentItems : List<GenericListItem<in ViewBinding>> get() = asyncListDiffer.currentList
var currentSearchTerm = "" var currentSearchTerm = ""
private val viewTypesMapping = mutableMapOf<GenericLayoutFactory, Int>() private val viewTypesMapping = mutableMapOf<ViewBindingFactory, Int>()
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) = GenericViewHolder(viewTypesMapping.filterValues { it == viewType }.keys.single().createLayout(parent)) override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) = GenericViewHolder(viewTypesMapping.filterValues { it == viewType }.keys.single().createBinding(parent))
override fun onBindViewHolder(holder : GenericViewHolder, position : Int) { override fun onBindViewHolder(holder : GenericViewHolder<ViewBinding>, position : Int) {
currentItems[position].apply { currentItems[position].apply {
adapter = this@GenericAdapter adapter = this@GenericAdapter
bind(holder, position) bind(holder, position)
@ -46,9 +48,9 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
override fun getItemCount() = currentItems.size override fun getItemCount() = currentItems.size
override fun getItemViewType(position : Int) = viewTypesMapping.getOrPut(currentItems[position].getLayoutFactory(), { viewTypesMapping.size }) override fun getItemViewType(position : Int) = viewTypesMapping.getOrPut(currentItems[position].getViewBindingFactory()) { viewTypesMapping.size }
fun setItems(items : List<GenericListItem>) { fun setItems(items : List<GenericListItem<*>>) {
allItems.clear() allItems.clear()
allItems.addAll(items) allItems.addAll(items)
filter.filter(currentSearchTerm) filter.filter(currentSearchTerm)
@ -68,7 +70,7 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
*/ */
private val cos = Cosine() private val cos = Cosine()
inner class ScoredItem(val score : Double, val item : GenericListItem) inner class ScoredItem(val score : Double, val item : GenericListItem<*>)
/** /**
* This sorts the items in [allItems] in relation to how similar they are to [currentSearchTerm] * This sorts the items in [allItems] in relation to how similar they are to [currentSearchTerm]
@ -93,7 +95,7 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
results.values = allItems.toMutableList() results.values = allItems.toMutableList()
results.count = allItems.size results.count = allItems.size
} else { } else {
val filterData = mutableListOf<GenericListItem>() val filterData = mutableListOf<GenericListItem<*>>()
val topResults = extractSorted() val topResults = extractSorted()
val avgScore = topResults.sumByDouble { it.score } / topResults.size val avgScore = topResults.sumByDouble { it.score } / topResults.size
@ -112,7 +114,7 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
*/ */
override fun publishResults(charSequence : CharSequence, results : FilterResults) { override fun publishResults(charSequence : CharSequence, results : FilterResults) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
asyncListDiffer.submitList(results.values as List<GenericListItem>) asyncListDiffer.submitList(results.values as List<GenericListItem<ViewBinding>>)
} }
} }
} }

View File

@ -5,33 +5,38 @@
package emu.skyline.adapter package emu.skyline.adapter
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer import androidx.viewbinding.ViewBinding
class GenericViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer class GenericViewHolder<out V : ViewBinding>(val binding : V) : RecyclerView.ViewHolder(binding.root)
interface GenericLayoutFactory { fun View.inflater() = LayoutInflater.from(context)!!
fun createLayout(parent : ViewGroup) : View
interface ViewBindingFactory {
fun createBinding(parent : ViewGroup) : ViewBinding
} }
abstract class GenericListItem { abstract class GenericListItem<V : ViewBinding> {
var adapter : GenericAdapter? = null var adapter : GenericAdapter? = null
abstract fun getLayoutFactory() : GenericLayoutFactory abstract fun getViewBindingFactory() : ViewBindingFactory
abstract fun bind(holder : GenericViewHolder, position : Int) abstract fun bind(holder : GenericViewHolder<V>, position : Int)
/** /**
* Used for filtering * Used for filtering
*/ */
open fun key() : String = "" open fun key() : String = ""
open fun areItemsTheSame(other : GenericListItem) = this == other open fun areItemsTheSame(other : GenericListItem<V>) = this == other
/** /**
* Will only be called when [areItemsTheSame] returns true, thus returning true by default * Will only be called when [areItemsTheSame] returns true, thus returning true by default
*/ */
open fun areContentsTheSame(other : GenericListItem) = true open fun areContentsTheSame(other : GenericListItem<V>) = true
open val fullSpan : Boolean = false
} }

View File

@ -5,24 +5,23 @@
package emu.skyline.adapter package emu.skyline.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import emu.skyline.R import emu.skyline.databinding.SectionItemBinding
import kotlinx.android.synthetic.main.section_item.*
private object HeaderLayoutFactory : GenericLayoutFactory { object HeaderBindingFactory : ViewBindingFactory {
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.section_item, parent, false) override fun createBinding(parent : ViewGroup) = SectionItemBinding.inflate(parent.inflater(), parent, false)
} }
class HeaderViewItem(private val text : String) : GenericListItem() { class HeaderViewItem(private val text : String) : GenericListItem<SectionItemBinding>() {
override fun getLayoutFactory() : GenericLayoutFactory = HeaderLayoutFactory override fun getViewBindingFactory() = HeaderBindingFactory
override fun bind(holder : GenericViewHolder, position : Int) { override fun bind(holder : GenericViewHolder<SectionItemBinding>, position : Int) {
holder.text_title.text = text holder.binding.textTitle.text = text
} }
override fun toString() = "" override fun toString() = ""
override fun areItemsTheSame(other : GenericListItem) = other is HeaderViewItem && text == other.text override fun areItemsTheSame(other : GenericListItem<SectionItemBinding>) = other is HeaderViewItem && text == other.text
override val fullSpan = true
} }

View File

@ -7,25 +7,51 @@ package emu.skyline.adapter
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import emu.skyline.R import androidx.viewbinding.ViewBinding
import kotlinx.android.synthetic.main.log_item.* import emu.skyline.databinding.LogItemBinding
import emu.skyline.databinding.LogItemCompactBinding
private data class LogLayoutFactory(private val compact : Boolean) : GenericLayoutFactory { data class LogBindingFactory(private val compact : Boolean) : ViewBindingFactory {
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) override fun createBinding(parent : ViewGroup) = if (compact) LogCompactBinding(parent) else LogBinding(parent)
} }
data class LogViewItem(private val compact : Boolean, private val message : String, private val level : String) : GenericListItem() { interface ILogBinding : ViewBinding {
override fun getLayoutFactory() : GenericLayoutFactory = LogLayoutFactory(compact) val binding : ViewBinding
override fun bind(holder : GenericViewHolder, position : Int) { override fun getRoot() = binding.root
holder.text_title.text = message
holder.text_subtitle?.text = level
holder.itemView.setOnClickListener { val textTitle : TextView
val textSubTitle : TextView?
}
class LogCompactBinding(parent : ViewGroup) : ILogBinding {
override val binding = LogItemCompactBinding.inflate(parent.inflater(), parent, false)
override val textTitle = binding.textTitle
override val textSubTitle : Nothing? = null
}
class LogBinding(parent : ViewGroup) : ILogBinding {
override val binding = LogItemBinding.inflate(parent.inflater(), parent, false)
override val textTitle = binding.textTitle
override val textSubTitle = binding.textSubtitle
}
data class LogViewItem(private val compact : Boolean, private val message : String, private val level : String) : GenericListItem<ILogBinding>() {
override fun getViewBindingFactory() = LogBindingFactory(compact)
override fun bind(holder : GenericViewHolder<ILogBinding>, position : Int) {
holder.binding.textTitle.text = message
holder.binding.textSubTitle?.text = level
holder.binding.root.setOnClickListener {
it.context.getSystemService(ClipboardManager::class.java).setPrimaryClip(ClipData.newPlainText("Log Message", "$message ($level)")) it.context.getSystemService(ClipboardManager::class.java).setPrimaryClip(ClipData.newPlainText("Log Message", "$message ($level)"))
Toast.makeText(it.context, "Copied to clipboard", Toast.LENGTH_LONG).show() Toast.makeText(it.context, "Copied to clipboard", Toast.LENGTH_LONG).show()
} }

View File

@ -7,25 +7,26 @@ package emu.skyline.adapter.controller
import emu.skyline.adapter.GenericListItem import emu.skyline.adapter.GenericListItem
import emu.skyline.adapter.GenericViewHolder import emu.skyline.adapter.GenericViewHolder
import emu.skyline.databinding.ControllerItemBinding
import emu.skyline.di.InputManagerProviderEntryPoint
import emu.skyline.input.ButtonGuestEvent import emu.skyline.input.ButtonGuestEvent
import emu.skyline.input.ButtonId import emu.skyline.input.ButtonId
import emu.skyline.input.InputManager
/** /**
* This item is used to display a particular [button] mapping for the controller * 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() { 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) { override fun bind(holder : GenericViewHolder<ControllerItemBinding>, position : Int) {
content = button.long?.let { holder.itemView.context.getString(it) } ?: button.toString() content = button.long?.let { holder.itemView.context.getString(it) } ?: button.toString()
val guestEvent = ButtonGuestEvent(controllerId, button) val guestEvent = ButtonGuestEvent(controllerId, button)
subContent = InputManager.eventMap.filter { it.value is ButtonGuestEvent && it.value == guestEvent }.keys.firstOrNull()?.toString() ?: "" subContent = InputManagerProviderEntryPoint.getInputManager(holder.binding.root.context).eventMap.filter { it.value is ButtonGuestEvent && it.value == guestEvent }.keys.firstOrNull()?.toString() ?: ""
super.bind(holder, position) super.bind(holder, position)
holder.itemView.setOnClickListener { onClick.invoke(this, position) } holder.binding.root.setOnClickListener { onClick.invoke(this, position) }
} }
override fun areItemsTheSame(other : GenericListItem) = other is ControllerButtonViewItem && controllerId == other.controllerId override fun areItemsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerButtonViewItem && controllerId == other.controllerId
override fun areContentsTheSame(other : GenericListItem) = other is ControllerButtonViewItem && button == other.button override fun areContentsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerButtonViewItem && button == other.button
} }

View File

@ -5,36 +5,34 @@
package emu.skyline.adapter.controller package emu.skyline.adapter.controller
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isGone import androidx.core.view.isGone
import emu.skyline.R
import emu.skyline.adapter.GenericLayoutFactory
import emu.skyline.adapter.GenericListItem import emu.skyline.adapter.GenericListItem
import emu.skyline.adapter.GenericViewHolder import emu.skyline.adapter.GenericViewHolder
import kotlinx.android.synthetic.main.controller_checkbox_item.* import emu.skyline.adapter.ViewBindingFactory
import emu.skyline.adapter.inflater
import emu.skyline.databinding.ControllerCheckboxItemBinding
private object ControllerCheckBoxLayoutFactory : GenericLayoutFactory { object ControllerCheckBoxBindingFactory : ViewBindingFactory {
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.controller_checkbox_item, parent, false) override fun createBinding(parent : ViewGroup) = ControllerCheckboxItemBinding.inflate(parent.inflater(), parent, false)
} }
class ControllerCheckBoxViewItem(var title : String, var summary : String, var checked : Boolean, private val onCheckedChange : (item : ControllerCheckBoxViewItem, position : Int) -> Unit) : GenericListItem() { class ControllerCheckBoxViewItem(var title : String, var summary : String, var checked : Boolean, private val onCheckedChange : (item : ControllerCheckBoxViewItem, position : Int) -> Unit) : GenericListItem<ControllerCheckboxItemBinding>() {
override fun getLayoutFactory() : GenericLayoutFactory = ControllerCheckBoxLayoutFactory override fun getViewBindingFactory() = ControllerCheckBoxBindingFactory
override fun bind(holder : GenericViewHolder, position : Int) { override fun bind(holder : GenericViewHolder<ControllerCheckboxItemBinding>, position : Int) {
holder.text_title.isGone = title.isEmpty() holder.binding.textTitle.isGone = title.isEmpty()
holder.text_title.text = title holder.binding.textTitle.text = title
holder.text_subtitle.isGone = summary.isEmpty() holder.binding.textSubtitle.isGone = summary.isEmpty()
holder.text_subtitle.text = summary holder.binding.textSubtitle.text = summary
holder.checkbox.isChecked = checked holder.binding.checkbox.isChecked = checked
holder.itemView.setOnClickListener { holder.itemView.setOnClickListener {
checked = !checked checked = !checked
onCheckedChange.invoke(this, position) onCheckedChange.invoke(this, position)
} }
} }
override fun areItemsTheSame(other : GenericListItem) = other is ControllerCheckBoxViewItem override fun areItemsTheSame(other : GenericListItem<ControllerCheckboxItemBinding>) = other is ControllerCheckBoxViewItem
override fun areContentsTheSame(other : GenericListItem) = other is ControllerCheckBoxViewItem && title == other.title && summary == other.summary && checked == other.checked override fun areContentsTheSame(other : GenericListItem<ControllerCheckboxItemBinding>) = other is ControllerCheckBoxViewItem && title == other.title && summary == other.summary && checked == other.checked
} }

View File

@ -8,8 +8,9 @@ package emu.skyline.adapter.controller
import emu.skyline.R import emu.skyline.R
import emu.skyline.adapter.GenericListItem import emu.skyline.adapter.GenericListItem
import emu.skyline.adapter.GenericViewHolder import emu.skyline.adapter.GenericViewHolder
import emu.skyline.databinding.ControllerItemBinding
import emu.skyline.di.InputManagerProviderEntryPoint
import emu.skyline.input.GeneralType import emu.skyline.input.GeneralType
import emu.skyline.input.InputManager
import emu.skyline.input.JoyConLeftController import emu.skyline.input.JoyConLeftController
/** /**
@ -18,9 +19,9 @@ import emu.skyline.input.JoyConLeftController
* @param type The type of controller setting this item is displaying * @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() { 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) { override fun bind(holder : GenericViewHolder<ControllerItemBinding>, position : Int) {
val context = holder.itemView.context val context = holder.itemView.context
val controller = InputManager.controllers[controllerId]!! val controller = InputManagerProviderEntryPoint.getInputManager(context).controllers[controllerId]!!
content = context.getString(type.stringRes) content = context.getString(type.stringRes)
subContent = when (type) { subContent = when (type) {
@ -40,7 +41,7 @@ class ControllerGeneralViewItem(private val controllerId : Int, val type : Gener
holder.itemView.setOnClickListener { onClick.invoke(this, position) } holder.itemView.setOnClickListener { onClick.invoke(this, position) }
} }
override fun areItemsTheSame(other : GenericListItem) = other is ControllerGeneralViewItem && controllerId == other.controllerId override fun areItemsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerGeneralViewItem && controllerId == other.controllerId
override fun areContentsTheSame(other : GenericListItem) = other is ControllerGeneralViewItem && type == other.type override fun areContentsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerGeneralViewItem && type == other.type
} }

View File

@ -8,32 +8,34 @@ package emu.skyline.adapter.controller
import emu.skyline.R import emu.skyline.R
import emu.skyline.adapter.GenericListItem import emu.skyline.adapter.GenericListItem
import emu.skyline.adapter.GenericViewHolder import emu.skyline.adapter.GenericViewHolder
import emu.skyline.databinding.ControllerItemBinding
import emu.skyline.di.InputManagerProviderEntryPoint
import emu.skyline.input.AxisGuestEvent import emu.skyline.input.AxisGuestEvent
import emu.skyline.input.ButtonGuestEvent import emu.skyline.input.ButtonGuestEvent
import emu.skyline.input.InputManager
import emu.skyline.input.StickId import emu.skyline.input.StickId
/** /**
* This item is used to display all information regarding a [stick] and it's mappings for the controller * 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()) { 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) { override fun bind(holder : GenericViewHolder<ControllerItemBinding>, position : Int) {
val context = holder.itemView.context val context = holder.itemView.context
val inputManager = InputManagerProviderEntryPoint.getInputManager(context)
val buttonGuestEvent = ButtonGuestEvent(controllerId, stick.button) 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) 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) 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) 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) 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) 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) 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) 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) 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) 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" 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"
@ -42,7 +44,7 @@ class ControllerStickViewItem(private val controllerId : Int, val stick : StickI
holder.itemView.setOnClickListener { onClick.invoke(this, position) } holder.itemView.setOnClickListener { onClick.invoke(this, position) }
} }
override fun areItemsTheSame(other : GenericListItem) = other is ControllerStickViewItem && controllerId == other.controllerId override fun areItemsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerStickViewItem && controllerId == other.controllerId
override fun areContentsTheSame(other : GenericListItem) = other is ControllerStickViewItem && stick == other.stick override fun areContentsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerStickViewItem && stick == other.stick
} }

View File

@ -8,13 +8,14 @@ package emu.skyline.adapter.controller
import emu.skyline.R import emu.skyline.R
import emu.skyline.adapter.GenericListItem import emu.skyline.adapter.GenericListItem
import emu.skyline.adapter.GenericViewHolder import emu.skyline.adapter.GenericViewHolder
import emu.skyline.databinding.ControllerItemBinding
import emu.skyline.input.ControllerType import emu.skyline.input.ControllerType
/** /**
* This item is used to display the [type] of the currently active controller * 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() { class ControllerTypeViewItem(private val type : ControllerType, private val onClick : (item : ControllerTypeViewItem, position : Int) -> Unit) : ControllerViewItem() {
override fun bind(holder : GenericViewHolder, position : Int) { override fun bind(holder : GenericViewHolder<ControllerItemBinding>, position : Int) {
val context = holder.itemView.context val context = holder.itemView.context
content = context.getString(R.string.controller_type) content = context.getString(R.string.controller_type)
@ -25,7 +26,7 @@ class ControllerTypeViewItem(private val type : ControllerType, private val onCl
holder.itemView.setOnClickListener { onClick.invoke(this, position) } holder.itemView.setOnClickListener { onClick.invoke(this, position) }
} }
override fun areItemsTheSame(other : GenericListItem) = other is ControllerTypeViewItem override fun areItemsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerTypeViewItem
override fun areContentsTheSame(other : GenericListItem) = other is ControllerTypeViewItem && type == other.type override fun areContentsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerTypeViewItem && type == other.type
} }

View File

@ -5,32 +5,31 @@
package emu.skyline.adapter.controller package emu.skyline.adapter.controller
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isGone import androidx.core.view.isGone
import emu.skyline.R
import emu.skyline.adapter.GenericLayoutFactory
import emu.skyline.adapter.GenericListItem import emu.skyline.adapter.GenericListItem
import emu.skyline.adapter.GenericViewHolder import emu.skyline.adapter.GenericViewHolder
import kotlinx.android.synthetic.main.controller_item.* import emu.skyline.adapter.ViewBindingFactory
import emu.skyline.adapter.inflater
import emu.skyline.databinding.ControllerItemBinding
import emu.skyline.input.InputManager
private object ControllerLayoutFactory : GenericLayoutFactory { object ControllerBindingFactory : ViewBindingFactory {
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.controller_item, parent, false) override fun createBinding(parent : ViewGroup) = ControllerItemBinding.inflate(parent.inflater(), parent, false)
} }
open class ControllerViewItem(var content : String = "", var subContent : String = "", private val onClick : (() -> Unit)? = null) : GenericListItem() { open class ControllerViewItem(var content : String = "", var subContent : String = "", private val onClick : (() -> Unit)? = null) : GenericListItem<ControllerItemBinding>() {
private var position = -1 private var position = -1
override fun getLayoutFactory() : GenericLayoutFactory = ControllerLayoutFactory override fun getViewBindingFactory() = ControllerBindingFactory
override fun bind(holder : GenericViewHolder, position : Int) { override fun bind(holder : GenericViewHolder<ControllerItemBinding>, position : Int) {
this.position = position this.position = position
holder.text_title.apply { holder.binding.textTitle.apply {
isGone = content.isEmpty() isGone = content.isEmpty()
text = content text = content
} }
holder.text_subtitle.apply { holder.binding.textSubtitle.apply {
isGone = subContent.isEmpty() isGone = subContent.isEmpty()
text = subContent text = subContent
} }
@ -39,7 +38,7 @@ open class ControllerViewItem(var content : String = "", var subContent : String
fun update() = adapter?.notifyItemChanged(position) fun update() = adapter?.notifyItemChanged(position)
override fun areItemsTheSame(other : GenericListItem) = other is ControllerViewItem override fun areItemsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerViewItem
override fun areContentsTheSame(other : GenericListItem) = other is ControllerViewItem && content == other.content && subContent == other.subContent override fun areContentsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerViewItem && content == other.content && subContent == other.subContent
} }

View File

@ -0,0 +1,18 @@
package emu.skyline.di
import android.content.Context
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import emu.skyline.input.InputManager
@EntryPoint
@InstallIn(SingletonComponent::class)
interface InputManagerProviderEntryPoint {
fun inputManager() : InputManager
companion object {
fun getInputManager(context : Context) = EntryPointAccessors.fromApplication(context, InputManagerProviderEntryPoint::class.java).inputManager()
}
}

View File

@ -12,23 +12,27 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import emu.skyline.R import emu.skyline.R
import emu.skyline.adapter.GenericAdapter import emu.skyline.adapter.GenericAdapter
import emu.skyline.adapter.GenericListItem import emu.skyline.adapter.GenericListItem
import emu.skyline.adapter.HeaderViewItem import emu.skyline.adapter.HeaderViewItem
import emu.skyline.adapter.controller.* import emu.skyline.adapter.controller.*
import emu.skyline.databinding.ControllerActivityBinding
import emu.skyline.input.dialog.ButtonDialog import emu.skyline.input.dialog.ButtonDialog
import emu.skyline.input.dialog.RumbleDialog import emu.skyline.input.dialog.RumbleDialog
import emu.skyline.input.dialog.StickDialog import emu.skyline.input.dialog.StickDialog
import emu.skyline.input.onscreen.OnScreenEditActivity import emu.skyline.input.onscreen.OnScreenEditActivity
import emu.skyline.utils.Settings import emu.skyline.utils.Settings
import kotlinx.android.synthetic.main.controller_activity.* import javax.inject.Inject
import kotlinx.android.synthetic.main.titlebar.*
/** /**
* This activity is used to change the settings for a specific controller * This activity is used to change the settings for a specific controller
*/ */
@AndroidEntryPoint
class ControllerActivity : AppCompatActivity() { class ControllerActivity : AppCompatActivity() {
private val binding by lazy { ControllerActivityBinding.inflate(layoutInflater) }
/** /**
* The index of the controller this activity manages * The index of the controller this activity manages
*/ */
@ -51,14 +55,17 @@ class ControllerActivity : AppCompatActivity() {
private val settings by lazy { Settings(this) } private val settings by lazy { Settings(this) }
@Inject
lateinit var inputManager : InputManager
/** /**
* This function updates the [adapter] based on information from [InputManager] * This function updates the [adapter] based on information from [InputManager]
*/ */
private fun update() { private fun update() {
val items = mutableListOf<GenericListItem>() val items = mutableListOf<GenericListItem<*>>()
try { try {
val controller = InputManager.controllers[id]!! val controller = inputManager.controllers[id]!!
items.add(ControllerTypeViewItem(controller.type, onControllerTypeClick)) items.add(ControllerTypeViewItem(controller.type, onControllerTypeClick))
@ -166,20 +173,20 @@ class ControllerActivity : AppCompatActivity() {
title = "${getString(R.string.config_controller)} #${id + 1}" title = "${getString(R.string.config_controller)} #${id + 1}"
setContentView(R.layout.controller_activity) setContentView(binding.root)
setSupportActionBar(toolbar) setSupportActionBar(binding.titlebar.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
val layoutManager = LinearLayoutManager(this) val layoutManager = LinearLayoutManager(this)
controller_list.layoutManager = layoutManager binding.controllerList.layoutManager = layoutManager
controller_list.adapter = adapter binding.controllerList.adapter = adapter
controller_list.addOnScrollListener(object : RecyclerView.OnScrollListener() { binding.controllerList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView : RecyclerView, dx : Int, dy : Int) { override fun onScrolled(recyclerView : RecyclerView, dx : Int, dy : Int) {
super.onScrolled(recyclerView, dx, dy) super.onScrolled(recyclerView, dx, dy)
if (layoutManager.findLastCompletelyVisibleItemPosition() == adapter.itemCount - 1) app_bar_layout.setExpanded(false) if (layoutManager.findLastCompletelyVisibleItemPosition() == adapter.itemCount - 1) binding.titlebar.appBarLayout.setExpanded(false)
} }
}) })
@ -190,12 +197,12 @@ class ControllerActivity : AppCompatActivity() {
* This causes the input file to be synced when the activity has been paused * This causes the input file to be synced when the activity has been paused
*/ */
override fun onPause() { override fun onPause() {
InputManager.syncFile() inputManager.syncFile()
super.onPause() super.onPause()
} }
private val onControllerTypeClick = { item : ControllerTypeViewItem, _ : Int -> private val onControllerTypeClick = { item : ControllerTypeViewItem, _ : Int ->
val controller = InputManager.controllers[id]!! val controller = inputManager.controllers[id]!!
val types = ControllerType.values().apply { if (id != 0) filter { !it.firstController } } val types = ControllerType.values().apply { if (id != 0) filter { !it.firstController } }
val typeNames = types.map { getString(it.stringRes) }.toTypedArray() val typeNames = types.map { getString(it.stringRes) }.toTypedArray()
@ -206,11 +213,11 @@ class ControllerActivity : AppCompatActivity() {
val selectedType = types[typeIndex] val selectedType = types[typeIndex]
if (controller.type != selectedType) { if (controller.type != selectedType) {
if (controller is JoyConLeftController) if (controller is JoyConLeftController)
controller.partnerId?.let { (InputManager.controllers[it] as JoyConRightController).partnerId = null } controller.partnerId?.let { (inputManager.controllers[it] as JoyConRightController).partnerId = null }
else if (controller is JoyConRightController) else if (controller is JoyConRightController)
controller.partnerId?.let { (InputManager.controllers[it] as JoyConLeftController).partnerId = null } controller.partnerId?.let { (inputManager.controllers[it] as JoyConLeftController).partnerId = null }
InputManager.controllers[id] = when (selectedType) { inputManager.controllers[id] = when (selectedType) {
ControllerType.None -> Controller(id, ControllerType.None) ControllerType.None -> Controller(id, ControllerType.None)
ControllerType.HandheldProController -> HandheldController(id) ControllerType.HandheldProController -> HandheldController(id)
ControllerType.ProController -> ProController(id) ControllerType.ProController -> ProController(id)
@ -230,9 +237,9 @@ class ControllerActivity : AppCompatActivity() {
private val onControllerGeneralClick = { item : ControllerGeneralViewItem, _ : Int -> private val onControllerGeneralClick = { item : ControllerGeneralViewItem, _ : Int ->
when (item.type) { when (item.type) {
GeneralType.PartnerJoyCon -> { GeneralType.PartnerJoyCon -> {
val controller = InputManager.controllers[id] as JoyConLeftController val controller = inputManager.controllers[id] as JoyConLeftController
val rJoyCons = InputManager.controllers.values.filter { it.type == ControllerType.JoyConRight } 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 rJoyConNames = (listOf(getString(R.string.none)) + rJoyCons.map { "${getString(R.string.controller)} #${it.id + 1}" }).toTypedArray()
val partnerNameIndex = controller.partnerId?.let { partnerId -> val partnerNameIndex = controller.partnerId?.let { partnerId ->
@ -242,12 +249,12 @@ class ControllerActivity : AppCompatActivity() {
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle(item.content) .setTitle(item.content)
.setSingleChoiceItems(rJoyConNames, partnerNameIndex) { dialog, index -> .setSingleChoiceItems(rJoyConNames, partnerNameIndex) { dialog, index ->
(InputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = null (inputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = null
controller.partnerId = if (index == 0) null else rJoyCons[index - 1].id controller.partnerId = if (index == 0) null else rJoyCons[index - 1].id
if (controller.partnerId != null) if (controller.partnerId != null)
(InputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = controller.id (inputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = controller.id
item.update() item.update()

View File

@ -7,16 +7,20 @@ package emu.skyline.input
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.* import java.io.*
import javax.inject.Inject
import javax.inject.Singleton
/** /**
* This object is used to manage all transactions with storing/retrieving data in relation to input * This object is used to manage all transactions with storing/retrieving data in relation to input
*/ */
object InputManager { @Singleton
class InputManager @Inject constructor(@ApplicationContext context : Context) {
/** /**
* The underlying [File] object with the input data * The underlying [File] object with the input data
*/ */
private lateinit var file : File private val file = File("${context.applicationInfo.dataDir}/input.bin")
/** /**
* A [HashMap] of all the controllers that contains their metadata * A [HashMap] of all the controllers that contains their metadata
@ -28,31 +32,31 @@ object InputManager {
*/ */
lateinit var eventMap : HashMap<HostEvent?, GuestEvent?> lateinit var eventMap : HashMap<HostEvent?, GuestEvent?>
fun init(context : Context) { init {
file = File("${context.applicationInfo.dataDir}/input.bin") run {
try {
try { if (file.exists() && file.length() != 0L) {
if (file.exists() && file.length() != 0L) { syncObjects()
syncObjects() return@run
return }
} catch (e : Exception) {
Log.e(this.toString(), e.localizedMessage ?: "InputManager cannot read \"${file.absolutePath}\"")
} }
} catch (e : Exception) {
Log.e(this.toString(), e.localizedMessage ?: "InputManager cannot read \"${file.absolutePath}\"") controllers = hashMapOf(
0 to Controller(0, ControllerType.HandheldProController),
1 to Controller(1, ControllerType.None),
2 to Controller(2, ControllerType.None),
3 to Controller(3, ControllerType.None),
4 to Controller(4, ControllerType.None),
5 to Controller(5, ControllerType.None),
6 to Controller(6, ControllerType.None),
7 to Controller(7, ControllerType.None))
eventMap = hashMapOf()
syncFile()
} }
controllers = hashMapOf(
0 to Controller(0, ControllerType.HandheldProController),
1 to Controller(1, ControllerType.None),
2 to Controller(2, ControllerType.None),
3 to Controller(3, ControllerType.None),
4 to Controller(4, ControllerType.None),
5 to Controller(5, ControllerType.None),
6 to Controller(6, ControllerType.None),
7 to Controller(7, ControllerType.None))
eventMap = hashMapOf()
syncFile()
} }
/** /**

View File

@ -13,10 +13,12 @@ import android.view.*
import android.view.animation.LinearInterpolator import android.view.animation.LinearInterpolator
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint
import emu.skyline.R import emu.skyline.R
import emu.skyline.adapter.controller.ControllerButtonViewItem import emu.skyline.adapter.controller.ControllerButtonViewItem
import emu.skyline.databinding.ButtonDialogBinding
import emu.skyline.input.* import emu.skyline.input.*
import kotlinx.android.synthetic.main.button_dialog.* import javax.inject.Inject
import kotlin.math.abs import kotlin.math.abs
/** /**
@ -24,11 +26,17 @@ import kotlin.math.abs
* *
* @param item This is used to hold the [ControllerButtonViewItem] between instances * @param item This is used to hold the [ControllerButtonViewItem] between instances
*/ */
@AndroidEntryPoint
class ButtonDialog @JvmOverloads constructor(private val item : ControllerButtonViewItem? = null) : BottomSheetDialogFragment() { class ButtonDialog @JvmOverloads constructor(private val item : ControllerButtonViewItem? = null) : BottomSheetDialogFragment() {
private lateinit var binding : ButtonDialogBinding
@Inject
lateinit var inputManager : InputManager
/** /**
* This inflates the layout of the dialog after initial view creation * This inflates the layout of the dialog after initial view creation
*/ */
override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) : View? = inflater.inflate(R.layout.button_dialog, container) override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) = ButtonDialogBinding.inflate(inflater).also { binding = it }.root
/** /**
* This expands the bottom sheet so that it's fully visible * This expands the bottom sheet so that it's fully visible
@ -48,20 +56,20 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
if (item != null && context is ControllerActivity) { if (item != null && context is ControllerActivity) {
val context = requireContext() as ControllerActivity val context = requireContext() as ControllerActivity
val controller = InputManager.controllers[context.id]!! val controller = inputManager.controllers[context.id]!!
// View focus handling so all input is always directed to this view // View focus handling so all input is always directed to this view
view?.requestFocus() view?.requestFocus()
view?.onFocusChangeListener = View.OnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() } view?.onFocusChangeListener = View.OnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
// Write the text for the button's icon // Write the text for the button's icon
button_text.text = item.button.short ?: item.button.toString() binding.buttonText.text = item.button.short ?: item.button.toString()
// Set up the reset button to clear out all entries corresponding to this button from [InputManager.eventMap] // Set up the reset button to clear out all entries corresponding to this button from [inputManager.eventMap]
button_reset.setOnClickListener { binding.buttonReset.setOnClickListener {
val guestEvent = ButtonGuestEvent(context.id, item.button) val guestEvent = ButtonGuestEvent(context.id, item.button)
InputManager.eventMap.filterValues { it is ButtonGuestEvent && it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) } inputManager.eventMap.filterValues { it is ButtonGuestEvent && it == guestEvent }.keys.forEach { inputManager.eventMap.remove(it) }
item.update() item.update()
@ -69,11 +77,11 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
} }
// Ensure that layout animations are proper // Ensure that layout animations are proper
button_layout.layoutTransition.setAnimateParentHierarchy(false) binding.buttonLayout.layoutTransition.setAnimateParentHierarchy(false)
button_layout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) binding.buttonLayout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
// We want the secondary progress bar to be visible through the first one // We want the secondary progress bar to be visible through the first one
button_seekbar.progressDrawable.alpha = 128 binding.buttonSeekbar.progressDrawable.alpha = 128
var deviceId : Int? = null // The ID of the currently selected device var deviceId : Int? = null // The ID of the currently selected device
var inputId : Int? = null // The key code/axis ID of the currently selected event var inputId : Int? = null // The key code/axis ID of the currently selected event
@ -99,19 +107,19 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
axisRunnable = null axisRunnable = null
} }
button_icon.animate().alpha(0.75f).setDuration(50).start() binding.buttonIcon.animate().alpha(0.75f).setDuration(50).start()
button_text.animate().alpha(0.9f).setDuration(50).start() binding.buttonText.animate().alpha(0.9f).setDuration(50).start()
button_title.text = getString(R.string.release_confirm) binding.buttonTitle.text = getString(R.string.release_confirm)
button_seekbar.visibility = View.GONE binding.buttonSeekbar.visibility = View.GONE
} else if (deviceId == event.deviceId && inputId == event.keyCode && event.action == KeyEvent.ACTION_UP) { } else if (deviceId == event.deviceId && inputId == event.keyCode && event.action == KeyEvent.ACTION_UP) {
// We serialize the current [deviceId] and [inputId] into a [KeyHostEvent] and map it to a corresponding [GuestEvent] on [KeyEvent.ACTION_UP] // We serialize the current [deviceId] and [inputId] into a [KeyHostEvent] and map it to a corresponding [GuestEvent] on [KeyEvent.ACTION_UP]
val hostEvent = KeyHostEvent(event.device.descriptor, event.keyCode) val hostEvent = KeyHostEvent(event.device.descriptor, event.keyCode)
var guestEvent = InputManager.eventMap[hostEvent] var guestEvent = inputManager.eventMap[hostEvent]
if (guestEvent is GuestEvent) { if (guestEvent is GuestEvent) {
InputManager.eventMap.remove(hostEvent) inputManager.eventMap.remove(hostEvent)
if (guestEvent is ButtonGuestEvent) if (guestEvent is ButtonGuestEvent)
context.buttonMap[guestEvent.button]?.update() context.buttonMap[guestEvent.button]?.update()
@ -121,9 +129,9 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
guestEvent = ButtonGuestEvent(context.id, item.button) guestEvent = ButtonGuestEvent(context.id, item.button)
InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) } inputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { inputManager.eventMap.remove(it) }
InputManager.eventMap[hostEvent] = guestEvent inputManager.eventMap[hostEvent] = guestEvent
item.update() item.update()
@ -165,8 +173,8 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
inputId = axis inputId = axis
axisPolarity = value >= 0 axisPolarity = value >= 0
button_title.text = getString(R.string.hold_confirm) binding.buttonTitle.text = getString(R.string.hold_confirm)
button_seekbar.visibility = View.VISIBLE binding.buttonSeekbar.visibility = View.VISIBLE
break break
} }
@ -175,10 +183,10 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
// If the currently active input is a valid axis // If the currently active input is a valid axis
if (axes.contains(inputId)) { if (axes.contains(inputId)) {
val value = event.getAxisValue(inputId!!) val value = event.getAxisValue(inputId!!)
val threshold = button_seekbar.progress / 100f val threshold = binding.buttonSeekbar.progress / 100f
// Update the secondary progress bar in [button_seekbar] based on the axis's value // Update the secondary progress bar in [button_seekbar] based on the axis's value
button_seekbar.secondaryProgress = (abs(value) * 100).toInt() binding.buttonSeekbar.secondaryProgress = (abs(value) * 100).toInt()
// If the axis value crosses the threshold then post [axisRunnable] with a delay and animate the views accordingly // If the axis value crosses the threshold then post [axisRunnable] with a delay and animate the views accordingly
if (abs(value) >= threshold) { if (abs(value) >= threshold) {
@ -186,10 +194,10 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
axisRunnable = Runnable { axisRunnable = Runnable {
val hostEvent = MotionHostEvent(event.device.descriptor, inputId!!, axisPolarity) val hostEvent = MotionHostEvent(event.device.descriptor, inputId!!, axisPolarity)
var guestEvent = InputManager.eventMap[hostEvent] var guestEvent = inputManager.eventMap[hostEvent]
if (guestEvent is GuestEvent) { if (guestEvent is GuestEvent) {
InputManager.eventMap.remove(hostEvent) inputManager.eventMap.remove(hostEvent)
if (guestEvent is ButtonGuestEvent) if (guestEvent is ButtonGuestEvent)
context.buttonMap[(guestEvent as ButtonGuestEvent).button]?.update() context.buttonMap[(guestEvent as ButtonGuestEvent).button]?.update()
@ -199,9 +207,9 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
guestEvent = ButtonGuestEvent(controller.id, item.button, threshold) guestEvent = ButtonGuestEvent(controller.id, item.button, threshold)
InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) } inputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { inputManager.eventMap.remove(it) }
InputManager.eventMap[hostEvent] = guestEvent inputManager.eventMap[hostEvent] = guestEvent
item.update() item.update()
@ -211,8 +219,8 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
axisHandler.postDelayed(axisRunnable!!, 1000) axisHandler.postDelayed(axisRunnable!!, 1000)
} }
button_icon.animate().alpha(0.85f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start() binding.buttonIcon.animate().alpha(0.85f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start()
button_text.animate().alpha(0.95f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start() binding.buttonText.animate().alpha(0.95f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start()
} else { } else {
// If the axis value is below the threshold, remove [axisRunnable] from it being posted and animate the views accordingly // If the axis value is below the threshold, remove [axisRunnable] from it being posted and animate the views accordingly
if (axisRunnable != null) { if (axisRunnable != null) {
@ -220,8 +228,8 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
axisRunnable = null axisRunnable = null
} }
button_icon.animate().alpha(0.25f).setDuration(50).start() binding.buttonIcon.animate().alpha(0.25f).setDuration(50).start()
button_text.animate().alpha(0.35f).setDuration(50).start() binding.buttonText.animate().alpha(0.35f).setDuration(50).start()
} }
} }

View File

@ -14,22 +14,30 @@ import android.view.*
import android.view.animation.LinearInterpolator import android.view.animation.LinearInterpolator
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint
import emu.skyline.R import emu.skyline.R
import emu.skyline.adapter.controller.ControllerGeneralViewItem import emu.skyline.adapter.controller.ControllerGeneralViewItem
import emu.skyline.databinding.RumbleDialogBinding
import emu.skyline.input.ControllerActivity import emu.skyline.input.ControllerActivity
import emu.skyline.input.InputManager import emu.skyline.input.InputManager
import kotlinx.android.synthetic.main.rumble_dialog.* import javax.inject.Inject
/** /**
* This dialog is used to set a device to pass on any rumble/force feedback data onto * 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 [ControllerGeneralViewItem] between instances * @param item This is used to hold the [ControllerGeneralViewItem] between instances
*/ */
@AndroidEntryPoint
class RumbleDialog @JvmOverloads constructor(val item : ControllerGeneralViewItem? = null) : BottomSheetDialogFragment() { class RumbleDialog @JvmOverloads constructor(val item : ControllerGeneralViewItem? = null) : BottomSheetDialogFragment() {
private lateinit var binding : RumbleDialogBinding
@Inject
lateinit var inputManager : InputManager
/** /**
* This inflates the layout of the dialog after initial view creation * This inflates the layout of the dialog after initial view creation
*/ */
override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) : View? = inflater.inflate(R.layout.rumble_dialog, container) override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) = RumbleDialogBinding.inflate(inflater).also { binding = it }.root
/** /**
* This expands the bottom sheet so that it's fully visible * This expands the bottom sheet so that it's fully visible
@ -49,10 +57,10 @@ class RumbleDialog @JvmOverloads constructor(val item : ControllerGeneralViewIte
if (item != null && context is ControllerActivity) { if (item != null && context is ControllerActivity) {
val context = requireContext() as ControllerActivity val context = requireContext() as ControllerActivity
val controller = InputManager.controllers[context.id]!! val controller = inputManager.controllers[context.id]!!
// Set up the reset button to clear out [Controller.rumbleDevice] when pressed // Set up the reset button to clear out [Controller.rumbleDevice] when pressed
rumble_reset.setOnClickListener { binding.rumbleReset.setOnClickListener {
controller.rumbleDeviceDescriptor = null controller.rumbleDeviceDescriptor = null
controller.rumbleDeviceName = null controller.rumbleDeviceName = null
item.update() item.update()
@ -61,10 +69,10 @@ class RumbleDialog @JvmOverloads constructor(val item : ControllerGeneralViewIte
} }
if (context.id == 0) { if (context.id == 0) {
rumble_builtin.visibility = View.VISIBLE binding.rumbleBuiltin.visibility = View.VISIBLE
if (!(context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).hasVibrator()) if (!(context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).hasVibrator())
rumble_builtin.isEnabled = false binding.rumbleBuiltin.isEnabled = false
rumble_builtin.setOnClickListener { binding.rumbleBuiltin.setOnClickListener {
controller.rumbleDeviceDescriptor = "builtin" controller.rumbleDeviceDescriptor = "builtin"
controller.rumbleDeviceName = getString(R.string.builtin_vibrator) controller.rumbleDeviceName = getString(R.string.builtin_vibrator)
item.update() item.update()
@ -74,8 +82,8 @@ class RumbleDialog @JvmOverloads constructor(val item : ControllerGeneralViewIte
} }
// Ensure that layout animations are proper // Ensure that layout animations are proper
rumble_layout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) binding.rumbleLayout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
rumble_controller.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) binding.rumbleController.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
var deviceId : Int? = null // The ID of the currently selected device var deviceId : Int? = null // The ID of the currently selected device
@ -88,20 +96,20 @@ class RumbleDialog @JvmOverloads constructor(val item : ControllerGeneralViewIte
when { when {
// If the device doesn't match the currently selected device then update the UI accordingly and set [deviceId] to the current device // If the device doesn't match the currently selected device then update the UI accordingly and set [deviceId] to the current device
deviceId != event.deviceId -> { deviceId != event.deviceId -> {
rumble_controller_name.text = event.device.name binding.rumbleControllerName.text = event.device.name
if (vibrator.hasVibrator()) { if (vibrator.hasVibrator()) {
rumble_controller_supported.text = getString(R.string.supported) binding.rumbleControllerSupported.text = getString(R.string.supported)
rumble_title.text = getString(R.string.confirm_button_again) binding.rumbleTitle.text = getString(R.string.confirm_button_again)
vibrator.vibrate(VibrationEffect.createOneShot(250, VibrationEffect.DEFAULT_AMPLITUDE)) vibrator.vibrate(VibrationEffect.createOneShot(250, VibrationEffect.DEFAULT_AMPLITUDE))
} else { } else {
rumble_controller_supported.text = getString(R.string.not_supported) binding.rumbleControllerSupported.text = getString(R.string.not_supported)
dialog?.setOnKeyListener { _, _, _ -> false } dialog?.setOnKeyListener { _, _, _ -> false }
rumble_reset.requestFocus() binding.rumbleReset.requestFocus()
} }
rumble_controller_icon.animate().apply { binding.rumbleControllerIcon.animate().apply {
interpolator = LinearInterpolator() interpolator = LinearInterpolator()
duration = 100 duration = 100
alpha(if (vibrator.hasVibrator()) 0.75f else 0.5f) alpha(if (vibrator.hasVibrator()) 0.75f else 0.5f)

View File

@ -14,12 +14,14 @@ import android.view.*
import android.view.animation.LinearInterpolator import android.view.animation.LinearInterpolator
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint
import emu.skyline.R import emu.skyline.R
import emu.skyline.adapter.controller.ControllerStickViewItem import emu.skyline.adapter.controller.ControllerStickViewItem
import emu.skyline.databinding.StickDialogBinding
import emu.skyline.input.* import emu.skyline.input.*
import emu.skyline.input.MotionHostEvent.Companion.axes import emu.skyline.input.MotionHostEvent.Companion.axes
import kotlinx.android.synthetic.main.stick_dialog.*
import java.util.* import java.util.*
import javax.inject.Inject
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
@ -28,6 +30,7 @@ import kotlin.math.max
* *
* @param item This is used to hold the [ControllerStickViewItem] between instances * @param item This is used to hold the [ControllerStickViewItem] between instances
*/ */
@AndroidEntryPoint
class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem? = null) : BottomSheetDialogFragment() { class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem? = null) : BottomSheetDialogFragment() {
/** /**
* This enumerates all of the stages this dialog can be in * This enumerates all of the stages this dialog can be in
@ -41,6 +44,8 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
Stick(R.string.stick_preview); Stick(R.string.stick_preview);
} }
private lateinit var binding : StickDialogBinding
/** /**
* This is the current stage of the dialog * This is the current stage of the dialog
*/ */
@ -61,12 +66,13 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
*/ */
private var animationStop = false private var animationStop = false
@Inject
lateinit var inputManager : InputManager
/** /**
* This inflates the layout of the dialog after initial view creation * This inflates the layout of the dialog after initial view creation
*/ */
override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) : View? { override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) = StickDialogBinding.inflate(inflater).also { binding = it }.root
return requireActivity().layoutInflater.inflate(R.layout.stick_dialog, container)
}
/** /**
* This expands the bottom sheet so that it's fully visible * This expands the bottom sheet so that it's fully visible
@ -91,7 +97,13 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
animationStop = false animationStop = false
stageAnimation?.let { handler.removeCallbacks(it) } stageAnimation?.let { handler.removeCallbacks(it) }
stick_container?.animate()?.scaleX(1f)?.scaleY(1f)?.alpha(1f)?.translationY(0f)?.translationX(0f)?.rotationX(0f)?.rotationY(0f)?.start() binding.stickContainer.animate()
.scaleX(1f).scaleY(1f)
.alpha(1f)
.translationY(0f).translationX(0f)
.rotationX(0f)
.rotationY(0f)
.start()
when (stage) { when (stage) {
DialogStage.Button -> { DialogStage.Button -> {
@ -99,7 +111,7 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
if (stage != DialogStage.Button || animationStop) if (stage != DialogStage.Button || animationStop)
return@Runnable return@Runnable
stick_container?.animate()?.scaleX(0.85f)?.scaleY(0.85f)?.alpha(1f)?.withEndAction { binding.stickContainer.animate().scaleX(0.85f).scaleY(0.85f).alpha(1f).withEndAction {
if (stage != DialogStage.Button || animationStop) if (stage != DialogStage.Button || animationStop)
return@withEndAction return@withEndAction
@ -107,7 +119,7 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
if (stage != DialogStage.Button || animationStop) if (stage != DialogStage.Button || animationStop)
return@Runnable return@Runnable
stick_container?.animate()?.scaleX(1f)?.scaleY(1f)?.alpha(0.85f)?.withEndAction { binding.stickContainer.animate().scaleX(1f).scaleY(1f).alpha(0.85f).withEndAction {
if (stage != DialogStage.Button || animationStop) if (stage != DialogStage.Button || animationStop)
return@withEndAction return@withEndAction
@ -129,7 +141,7 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
if ((stage != DialogStage.YPlus && stage != DialogStage.YMinus) || animationStop) if ((stage != DialogStage.YPlus && stage != DialogStage.YMinus) || animationStop)
return@Runnable return@Runnable
stick_container?.animate()?.setDuration(300)?.translationY(dipToPixels(15f) * polarity)?.rotationX(27f * polarity)?.alpha(1f)?.withEndAction { binding.stickContainer.animate().setDuration(300).translationY(dipToPixels(15f) * polarity).rotationX(27f * polarity).alpha(1f).withEndAction {
if ((stage != DialogStage.YPlus && stage != DialogStage.YMinus) || animationStop) if ((stage != DialogStage.YPlus && stage != DialogStage.YMinus) || animationStop)
return@withEndAction return@withEndAction
@ -137,7 +149,7 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
if ((stage != DialogStage.YPlus && stage != DialogStage.YMinus) || animationStop) if ((stage != DialogStage.YPlus && stage != DialogStage.YMinus) || animationStop)
return@Runnable return@Runnable
stick_container?.animate()?.setDuration(250)?.translationY(0f)?.rotationX(0f)?.alpha(0.85f)?.withEndAction { binding.stickContainer.animate().setDuration(250).translationY(0f).rotationX(0f).alpha(0.85f).withEndAction {
if ((stage != DialogStage.YPlus && stage != DialogStage.YMinus) || animationStop) if ((stage != DialogStage.YPlus && stage != DialogStage.YMinus) || animationStop)
return@withEndAction return@withEndAction
@ -159,7 +171,7 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
if ((stage != DialogStage.XPlus && stage != DialogStage.XMinus) || animationStop) if ((stage != DialogStage.XPlus && stage != DialogStage.XMinus) || animationStop)
return@Runnable return@Runnable
stick_container?.animate()?.setDuration(300)?.translationX(dipToPixels(16.5f) * polarity)?.rotationY(27f * polarity)?.alpha(1f)?.withEndAction { binding.stickContainer.animate().setDuration(300).translationX(dipToPixels(16.5f) * polarity).rotationY(27f * polarity).alpha(1f).withEndAction {
if ((stage != DialogStage.XPlus && stage != DialogStage.XMinus) || animationStop) if ((stage != DialogStage.XPlus && stage != DialogStage.XMinus) || animationStop)
return@withEndAction return@withEndAction
@ -167,14 +179,14 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
if ((stage != DialogStage.XPlus && stage != DialogStage.XMinus) || animationStop) if ((stage != DialogStage.XPlus && stage != DialogStage.XMinus) || animationStop)
return@Runnable return@Runnable
stick_container?.animate()?.setDuration(250)?.translationX(0f)?.rotationY(0f)?.alpha(0.85f)?.withEndAction { binding.stickContainer.animate().setDuration(250).translationX(0f).rotationY(0f).alpha(0.85f).withEndAction {
if ((stage != DialogStage.XPlus && stage != DialogStage.XMinus) || animationStop) if ((stage != DialogStage.XPlus && stage != DialogStage.XMinus) || animationStop)
return@withEndAction return@withEndAction
stageAnimation?.let { stageAnimation?.let {
handler.postDelayed(it, 750) handler.postDelayed(it, 750)
} }
}?.start() }.start()
} }
handler.postDelayed(runnable, 300) handler.postDelayed(runnable, 300)
@ -199,13 +211,13 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
if (ordinal in 0 until size) { if (ordinal in 0 until size) {
stage = DialogStage.values()[ordinal] stage = DialogStage.values()[ordinal]
stick_title.text = getString(stage.string) binding.stickTitle.text = getString(stage.string)
stick_subtitle.text = if (stage != DialogStage.Stick) getString(R.string.use_button_axis) else getString(R.string.use_non_stick) binding.stickSubtitle.text = getString(if (stage != DialogStage.Stick) R.string.use_button_axis else R.string.use_non_stick)
stick_icon.animate().alpha(0.25f).setDuration(50).start() binding.stickIcon.animate().alpha(0.25f).setDuration(50).start()
stick_name.animate().alpha(0.35f).setDuration(50).start() binding.stickName.animate().alpha(0.35f).setDuration(50).start()
stick_seekbar.visibility = View.GONE binding.stickSeekbar.visibility = View.GONE
stick_next.text = if (ordinal + 1 == size) getString(R.string.done) else getString(R.string.next) binding.stickNext.text = getString(if (ordinal + 1 == size) R.string.done else R.string.next)
updateAnimation() updateAnimation()
} else { } else {
@ -221,28 +233,28 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
if (item != null && context is ControllerActivity) { if (item != null && context is ControllerActivity) {
val context = requireContext() as ControllerActivity val context = requireContext() as ControllerActivity
val controller = InputManager.controllers[context.id]!! val controller = inputManager.controllers[context.id]!!
// View focus handling so all input is always directed to this view // View focus handling so all input is always directed to this view
view?.requestFocus() view?.requestFocus()
view?.onFocusChangeListener = View.OnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() } view?.onFocusChangeListener = View.OnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
// Write the text for the stick's icon // Write the text for the stick's icon
stick_name.text = item.stick.button.short ?: item.stick.button.toString() binding.stickName.text = item.stick.button.short ?: item.stick.button.toString()
// Set up the reset button to clear out all entries corresponding to this stick from [InputManager.eventMap] // Set up the reset button to clear out all entries corresponding to this stick from [inputManager.eventMap]
stick_reset.setOnClickListener { binding.stickReset.setOnClickListener {
for (axis in arrayOf(item.stick.xAxis, item.stick.yAxis)) { for (axis in arrayOf(item.stick.xAxis, item.stick.yAxis)) {
for (polarity in booleanArrayOf(true, false)) { for (polarity in booleanArrayOf(true, false)) {
val guestEvent = AxisGuestEvent(context.id, axis, polarity) val guestEvent = AxisGuestEvent(context.id, axis, polarity)
InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) } inputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { inputManager.eventMap.remove(it) }
} }
} }
val guestEvent = ButtonGuestEvent(context.id, item.stick.button) val guestEvent = ButtonGuestEvent(context.id, item.stick.button)
InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) } inputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { inputManager.eventMap.remove(it) }
item.update() item.update()
@ -250,11 +262,11 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
} }
// Ensure that layout animations are proper // Ensure that layout animations are proper
stick_layout.layoutTransition.setAnimateParentHierarchy(false) binding.stickLayout.layoutTransition.setAnimateParentHierarchy(false)
stick_layout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING) binding.stickLayout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
// We want the secondary progress bar to be visible through the first one // We want the secondary progress bar to be visible through the first one
stick_seekbar.progressDrawable.alpha = 128 binding.stickSeekbar.progressDrawable.alpha = 128
updateAnimation() updateAnimation()
@ -266,7 +278,7 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
var axisPolarity = false // The polarity of the axis for the currently selected event var axisPolarity = false // The polarity of the axis for the currently selected event
var axisRunnable : Runnable? = null // The Runnable that is used for counting down till an axis is selected var axisRunnable : Runnable? = null // The Runnable that is used for counting down till an axis is selected
stick_next.setOnClickListener { binding.stickNext.setOnClickListener {
gotoStage(1) gotoStage(1)
deviceId = null deviceId = null
@ -280,39 +292,41 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
// We want all input events from Joysticks and Buttons except for [KeyEvent.KEYCODE_BACK] as that will should be processed elsewhere // We want all input events from Joysticks and Buttons except for [KeyEvent.KEYCODE_BACK] as that will should be processed elsewhere
((event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON) && event.keyCode != KeyEvent.KEYCODE_BACK) || event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK)) && event.repeatCount == 0 -> { ((event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON) && event.keyCode != KeyEvent.KEYCODE_BACK) || event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK)) && event.repeatCount == 0 -> {
if (stage == DialogStage.Stick) { if (stage == DialogStage.Stick) {
// When the stick is being previewed after everything is mapped we do a lookup into [InputManager.eventMap] to find a corresponding [GuestEvent] and animate the stick correspondingly // When the stick is being previewed after everything is mapped we do a lookup into [inputManager.eventMap] to find a corresponding [GuestEvent] and animate the stick correspondingly
when (val guestEvent = InputManager.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) { when (val guestEvent = inputManager.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) {
is ButtonGuestEvent -> { is ButtonGuestEvent -> {
if (guestEvent.button == item.stick.button) { if (guestEvent.button == item.stick.button) {
if (event.action == KeyEvent.ACTION_DOWN) { if (event.action == KeyEvent.ACTION_DOWN) {
stick_container?.animate()?.setStartDelay(0)?.setDuration(50)?.scaleX(0.85f)?.scaleY(0.85f)?.start() binding.stickContainer.animate().setStartDelay(0).setDuration(50).scaleX(0.85f).scaleY(0.85f).start()
stick_icon.animate().alpha(0.85f).setDuration(50).start() binding.stickIcon.animate().alpha(0.85f).setDuration(50).start()
stick_name.animate().alpha(0.95f).setDuration(50).start() binding.stickName.animate().alpha(0.95f).setDuration(50).start()
} else { } else {
stick_container?.animate()?.setStartDelay(0)?.setDuration(25)?.scaleX(1f)?.scaleY(1f)?.start() binding.stickContainer.animate().setStartDelay(0).setDuration(25).scaleX(1f).scaleY(1f).start()
stick_icon.animate().alpha(0.25f).setDuration(25).start() binding.stickIcon.animate().alpha(0.25f).setDuration(25).start()
stick_name.animate().alpha(0.35f).setDuration(25).start() binding.stickName.animate().alpha(0.35f).setDuration(25).start()
} }
} else if (event.action == KeyEvent.ACTION_UP) { } else if (event.action == KeyEvent.ACTION_UP) {
stick_next?.callOnClick() binding.stickNext.callOnClick()
} }
} }
is AxisGuestEvent -> { is AxisGuestEvent -> {
val coefficient = if (event.action == KeyEvent.ACTION_DOWN) if (guestEvent.polarity) 1 else -1 else 0 val coefficient = if (event.action == KeyEvent.ACTION_DOWN) if (guestEvent.polarity) 1 else -1 else 0
if (guestEvent.axis == item.stick.xAxis) { binding.stickContainer.apply {
stick_container?.translationX = dipToPixels(16.5f) * coefficient if (guestEvent.axis == item.stick.xAxis) {
stick_container?.rotationY = 27f * coefficient translationX = dipToPixels(16.5f) * coefficient
} else if (guestEvent.axis == item.stick.yAxis) { rotationY = 27f * coefficient
stick_container?.translationY = dipToPixels(16.5f) * -coefficient } else if (guestEvent.axis == item.stick.yAxis) {
stick_container?.rotationX = 27f * coefficient translationY = dipToPixels(16.5f) * -coefficient
rotationX = 27f * coefficient
}
} }
} }
null -> if (event.action == KeyEvent.ACTION_UP) stick_next?.callOnClick() null -> if (event.action == KeyEvent.ACTION_UP) binding.stickNext.callOnClick()
} }
} else if (stage != DialogStage.Stick) { } else if (stage != DialogStage.Stick) {
if ((deviceId != event.deviceId || inputId != event.keyCode) && event.action == KeyEvent.ACTION_DOWN && !ignoredEvents.any { it == Objects.hash(event.deviceId, event.keyCode) }) { if ((deviceId != event.deviceId || inputId != event.keyCode) && event.action == KeyEvent.ACTION_DOWN && !ignoredEvents.any { it == Objects.hash(event.deviceId, event.keyCode) }) {
@ -330,26 +344,28 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
val coefficient = if (stage == DialogStage.YMinus || stage == DialogStage.XPlus) 1 else -1 val coefficient = if (stage == DialogStage.YMinus || stage == DialogStage.XPlus) 1 else -1
when (stage) { when (stage) {
DialogStage.Button -> stick_container?.animate()?.setStartDelay(0)?.setDuration(25)?.scaleX(0.85f)?.scaleY(0.85f)?.alpha(1f)?.start() DialogStage.Button -> binding.stickContainer.animate().setStartDelay(0).setDuration(25).scaleX(0.85f).scaleY(0.85f).alpha(1f).start()
DialogStage.YPlus, DialogStage.YMinus -> stick_container?.animate()?.setStartDelay(0)?.setDuration(75)?.translationY(dipToPixels(16.5f) * coefficient)?.rotationX(27f * coefficient)?.alpha(1f)?.start()
DialogStage.XPlus, DialogStage.XMinus -> stick_container?.animate()?.setStartDelay(0)?.setDuration(75)?.translationX(dipToPixels(16.5f) * coefficient)?.rotationY(27f * coefficient)?.alpha(1f)?.start() DialogStage.YPlus, DialogStage.YMinus -> binding.stickContainer.animate().setStartDelay(0).setDuration(75).translationY(dipToPixels(16.5f) * coefficient).rotationX(27f * coefficient).alpha(1f).start()
DialogStage.XPlus, DialogStage.XMinus -> binding.stickContainer.animate().setStartDelay(0).setDuration(75).translationX(dipToPixels(16.5f) * coefficient).rotationY(27f * coefficient).alpha(1f).start()
else -> { else -> {
} }
} }
stick_icon.animate().alpha(0.85f).setDuration(50).start() binding.stickIcon.animate().alpha(0.85f).setDuration(50).start()
stick_name.animate().alpha(0.95f).setDuration(50).start() binding.stickName.animate().alpha(0.95f).setDuration(50).start()
stick_subtitle.text = getString(R.string.release_confirm) binding.stickSubtitle.text = getString(R.string.release_confirm)
stick_seekbar.visibility = View.GONE binding.stickSeekbar.visibility = View.GONE
} else if (deviceId == event.deviceId && inputId == event.keyCode && event.action == KeyEvent.ACTION_UP) { } else if (deviceId == event.deviceId && inputId == event.keyCode && event.action == KeyEvent.ACTION_UP) {
// We serialize the current [deviceId] and [inputId] into a [KeyHostEvent] and map it to a corresponding [GuestEvent] and add it to [ignoredEvents] on [KeyEvent.ACTION_UP] // We serialize the current [deviceId] and [inputId] into a [KeyHostEvent] and map it to a corresponding [GuestEvent] and add it to [ignoredEvents] on [KeyEvent.ACTION_UP]
val hostEvent = KeyHostEvent(event.device.descriptor, event.keyCode) val hostEvent = KeyHostEvent(event.device.descriptor, event.keyCode)
var guestEvent = InputManager.eventMap[hostEvent] var guestEvent = inputManager.eventMap[hostEvent]
if (guestEvent is GuestEvent) { if (guestEvent is GuestEvent) {
InputManager.eventMap.remove(hostEvent) inputManager.eventMap.remove(hostEvent)
if (guestEvent is ButtonGuestEvent) if (guestEvent is ButtonGuestEvent)
context.buttonMap[guestEvent.button]?.update() context.buttonMap[guestEvent.button]?.update()
@ -364,15 +380,15 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
else -> null else -> null
} }
InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) } inputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { inputManager.eventMap.remove(it) }
InputManager.eventMap[hostEvent] = guestEvent inputManager.eventMap[hostEvent] = guestEvent
ignoredEvents.add(Objects.hash(deviceId!!, inputId!!)) ignoredEvents.add(Objects.hash(deviceId!!, inputId!!))
item.update() item.update()
stick_next?.callOnClick() binding.stickNext.callOnClick()
} }
} }
@ -407,7 +423,7 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
// We want all input events from Joysticks and Buttons that are [MotionEvent.ACTION_MOVE] and not from the D-pad // We want all input events from Joysticks and Buttons that are [MotionEvent.ACTION_MOVE] and not from the D-pad
if ((event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK) || event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON)) && event.action == MotionEvent.ACTION_MOVE && hat == oldHat) { if ((event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK) || event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON)) && event.action == MotionEvent.ACTION_MOVE && hat == oldHat) {
if (stage == DialogStage.Stick) { if (stage == DialogStage.Stick) {
// When the stick is being previewed after everything is mapped we do a lookup into [InputManager.eventMap] to find a corresponding [GuestEvent] and animate the stick correspondingly // When the stick is being previewed after everything is mapped we do a lookup into [inputManager.eventMap] to find a corresponding [GuestEvent] and animate the stick correspondingly
for (axisItem in axes.withIndex()) { for (axisItem in axes.withIndex()) {
val axis = axisItem.value val axis = axisItem.value
var value = event.getAxisValue(axis) var value = event.getAxisValue(axis)
@ -421,9 +437,9 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
var polarity = value >= 0 var polarity = value >= 0
val guestEvent = MotionHostEvent(event.device.descriptor, axis, polarity).let { hostEvent -> val guestEvent = MotionHostEvent(event.device.descriptor, axis, polarity).let { hostEvent ->
InputManager.eventMap[hostEvent] ?: if (value == 0f) { inputManager.eventMap[hostEvent] ?: if (value == 0f) {
polarity = false polarity = false
InputManager.eventMap[hostEvent.copy(polarity = false)] inputManager.eventMap[hostEvent.copy(polarity = false)]
} else { } else {
null null
} }
@ -433,13 +449,13 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
is ButtonGuestEvent -> { is ButtonGuestEvent -> {
if (guestEvent.button == item.stick.button) { if (guestEvent.button == item.stick.button) {
if (abs(value) >= guestEvent.threshold) { if (abs(value) >= guestEvent.threshold) {
stick_container?.animate()?.setStartDelay(0)?.setDuration(50)?.scaleX(0.85f)?.scaleY(0.85f)?.start() binding.stickContainer.animate().setStartDelay(0).setDuration(50).scaleX(0.85f).scaleY(0.85f).start()
stick_icon.animate().alpha(0.85f).setDuration(50).start() binding.stickIcon.animate().alpha(0.85f).setDuration(50).start()
stick_name.animate().alpha(0.95f).setDuration(50).start() binding.stickName.animate().alpha(0.95f).setDuration(50).start()
} else { } else {
stick_container?.animate()?.setStartDelay(0)?.setDuration(25)?.scaleX(1f)?.scaleY(1f)?.start() binding.stickContainer.animate().setStartDelay(0).setDuration(25).scaleX(1f).scaleY(1f).start()
stick_icon.animate().alpha(0.25f).setDuration(25).start() binding.stickIcon.animate().alpha(0.25f).setDuration(25).start()
stick_name.animate().alpha(0.35f).setDuration(25).start() binding.stickName.animate().alpha(0.35f).setDuration(25).start()
} }
} }
} }
@ -449,12 +465,14 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
val coefficient = if (polarity) abs(value) else -abs(value) val coefficient = if (polarity) abs(value) else -abs(value)
if (guestEvent.axis == item.stick.xAxis) { binding.stickContainer.apply {
stick_container?.translationX = dipToPixels(16.5f) * coefficient if (guestEvent.axis == item.stick.xAxis) {
stick_container?.rotationY = 27f * coefficient translationX = dipToPixels(16.5f) * coefficient
} else if (guestEvent.axis == item.stick.yAxis) { rotationY = 27f * coefficient
stick_container?.translationY = dipToPixels(16.5f) * coefficient } else if (guestEvent.axis == item.stick.yAxis) {
stick_container?.rotationX = 27f * -coefficient translationY = dipToPixels(16.5f) * coefficient
rotationX = 27f * -coefficient
}
} }
} }
} }
@ -480,10 +498,10 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
inputId = axis inputId = axis
axisPolarity = value >= 0 axisPolarity = value >= 0
stick_subtitle.text = getString(R.string.hold_confirm) binding.stickSubtitle.text = getString(R.string.hold_confirm)
if (stage == DialogStage.Button) if (stage == DialogStage.Button)
stick_seekbar.visibility = View.VISIBLE binding.stickSeekbar.visibility = View.VISIBLE
animationStop = true animationStop = true
@ -494,13 +512,13 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
// If the currently active input is a valid axis // If the currently active input is a valid axis
if (axes.contains(inputId)) { if (axes.contains(inputId)) {
val value = event.getAxisValue(inputId!!) val value = event.getAxisValue(inputId!!)
val threshold = if (stage == DialogStage.Button) stick_seekbar.progress / 100f else 0.5f val threshold = if (stage == DialogStage.Button) binding.stickSeekbar.progress / 100f else 0.5f
when (stage) { when (stage) {
// Update the secondary progress bar in [button_seekbar] based on the axis's value // Update the secondary progress bar in [button_seekbar] based on the axis's value
DialogStage.Button -> { DialogStage.Button -> {
stick_container?.animate()?.setStartDelay(0)?.setDuration(25)?.scaleX(0.85f)?.scaleY(0.85f)?.alpha(1f)?.start() binding.stickContainer.animate().setStartDelay(0).setDuration(25).scaleX(0.85f).scaleY(0.85f).alpha(1f).start()
stick_seekbar.secondaryProgress = (abs(value) * 100).toInt() binding.stickSeekbar.secondaryProgress = (abs(value) * 100).toInt()
} }
@ -508,16 +526,16 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
DialogStage.YPlus, DialogStage.YMinus -> { DialogStage.YPlus, DialogStage.YMinus -> {
val coefficient = if (stage == DialogStage.YMinus) abs(value) else -abs(value) val coefficient = if (stage == DialogStage.YMinus) abs(value) else -abs(value)
stick_container?.translationY = dipToPixels(16.5f) * coefficient binding.stickContainer.translationY = dipToPixels(16.5f) * coefficient
stick_container?.rotationX = 27f * -coefficient binding.stickContainer.rotationX = 27f * -coefficient
} }
// Update the the position of the stick in the X-axis based on the axis's value // Update the the position of the stick in the X-axis based on the axis's value
DialogStage.XPlus, DialogStage.XMinus -> { DialogStage.XPlus, DialogStage.XMinus -> {
val coefficient = if (stage == DialogStage.XPlus) abs(value) else -abs(value) val coefficient = if (stage == DialogStage.XPlus) abs(value) else -abs(value)
stick_container?.translationX = dipToPixels(16.5f) * coefficient binding.stickContainer.translationX = dipToPixels(16.5f) * coefficient
stick_container?.rotationY = 27f * coefficient binding.stickContainer.rotationY = 27f * coefficient
} }
else -> { else -> {
@ -530,10 +548,10 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
axisRunnable = Runnable { axisRunnable = Runnable {
val hostEvent = MotionHostEvent(event.device.descriptor, inputId!!, axisPolarity) val hostEvent = MotionHostEvent(event.device.descriptor, inputId!!, axisPolarity)
var guestEvent = InputManager.eventMap[hostEvent] var guestEvent = inputManager.eventMap[hostEvent]
if (guestEvent is GuestEvent) { if (guestEvent is GuestEvent) {
InputManager.eventMap.remove(hostEvent) inputManager.eventMap.remove(hostEvent)
if (guestEvent is ButtonGuestEvent) if (guestEvent is ButtonGuestEvent)
context.buttonMap[(guestEvent as ButtonGuestEvent).button]?.update() context.buttonMap[(guestEvent as ButtonGuestEvent).button]?.update()
@ -550,9 +568,9 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
else -> null else -> null
} }
InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) } inputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { inputManager.eventMap.remove(it) }
InputManager.eventMap[hostEvent] = guestEvent inputManager.eventMap[hostEvent] = guestEvent
ignoredEvents.add(Objects.hash(deviceId!!, inputId!!, axisPolarity)) ignoredEvents.add(Objects.hash(deviceId!!, inputId!!, axisPolarity))
@ -560,14 +578,14 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
item.update() item.update()
stick_next?.callOnClick() binding.stickNext.callOnClick()
} }
handler.postDelayed(axisRunnable!!, 1000) handler.postDelayed(axisRunnable!!, 1000)
} }
stick_icon.animate().alpha(0.85f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start() binding.stickIcon.animate().alpha(0.85f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start()
stick_name.animate().alpha(0.95f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start() binding.stickName.animate().alpha(0.95f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start()
} else { } else {
// If the axis value is below the threshold, remove [axisRunnable] from it being posted and animate the views accordingly // If the axis value is below the threshold, remove [axisRunnable] from it being posted and animate the views accordingly
if (axisRunnable != null) { if (axisRunnable != null) {
@ -576,10 +594,10 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
} }
if (stage == DialogStage.Button) if (stage == DialogStage.Button)
stick_container?.animate()?.setStartDelay(0)?.setDuration(10)?.scaleX(1f)?.scaleY(1f)?.alpha(1f)?.start() binding.stickContainer.animate().setStartDelay(0).setDuration(10).scaleX(1f).scaleY(1f).alpha(1f).start()
stick_icon.animate().alpha(0.25f).setDuration(50).start() binding.stickIcon.animate().alpha(0.25f).setDuration(50).start()
stick_name.animate().alpha(0.35f).setDuration(50).start() binding.stickName.animate().alpha(0.35f).setDuration(50).start()
} }
} }
} }

View File

@ -16,17 +16,19 @@ import androidx.core.content.ContextCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import emu.skyline.R import emu.skyline.R
import emu.skyline.databinding.OnScreenEditActivityBinding
import emu.skyline.utils.Settings import emu.skyline.utils.Settings
import kotlinx.android.synthetic.main.on_screen_edit_activity.*
class OnScreenEditActivity : AppCompatActivity() { class OnScreenEditActivity : AppCompatActivity() {
private val binding by lazy { OnScreenEditActivityBinding.inflate(layoutInflater) }
private var fullEditVisible = true private var fullEditVisible = true
private var editMode = false private var editMode = false
private val closeAction : () -> Unit = { private val closeAction : () -> Unit = {
if (editMode) { if (editMode) {
toggleFabVisibility(true) toggleFabVisibility(true)
on_screen_controller_view.setEditMode(false) binding.onScreenControllerView.setEditMode(false)
editMode = false editMode = false
} else { } else {
fullEditVisible = !fullEditVisible fullEditVisible = !fullEditVisible
@ -45,12 +47,12 @@ class OnScreenEditActivity : AppCompatActivity() {
private val editAction = { private val editAction = {
editMode = true editMode = true
on_screen_controller_view.setEditMode(true) binding.onScreenControllerView.setEditMode(true)
toggleFabVisibility(false) toggleFabVisibility(false)
} }
private val toggleAction : () -> Unit = { private val toggleAction : () -> Unit = {
val buttonProps = on_screen_controller_view.getButtonProps() val buttonProps = binding.onScreenControllerView.getButtonProps()
val checkArray = buttonProps.map { it.second }.toBooleanArray() val checkArray = buttonProps.map { it.second }.toBooleanArray()
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
@ -62,7 +64,7 @@ class OnScreenEditActivity : AppCompatActivity() {
}.setPositiveButton(R.string.confirm) { _, _ -> }.setPositiveButton(R.string.confirm) { _, _ ->
buttonProps.forEachIndexed { index, pair -> buttonProps.forEachIndexed { index, pair ->
if (checkArray[index] != pair.second) if (checkArray[index] != pair.second)
on_screen_controller_view.setButtonEnabled(pair.first, checkArray[index]) binding.onScreenControllerView.setButtonEnabled(pair.first, checkArray[index])
} }
}.setNegativeButton(R.string.cancel, null) }.setNegativeButton(R.string.cancel, null)
.setOnDismissListener { fullScreen() } .setOnDismissListener { fullScreen() }
@ -70,11 +72,11 @@ class OnScreenEditActivity : AppCompatActivity() {
} }
private val actions : List<Pair<Int, () -> Unit>> = listOf( private val actions : List<Pair<Int, () -> Unit>> = listOf(
Pair(R.drawable.ic_restore, { on_screen_controller_view.resetControls() }), Pair(R.drawable.ic_restore, { binding.onScreenControllerView.resetControls() }),
Pair(R.drawable.ic_toggle, toggleAction), Pair(R.drawable.ic_toggle, toggleAction),
Pair(R.drawable.ic_edit, editAction), Pair(R.drawable.ic_edit, editAction),
Pair(R.drawable.ic_zoom_out, { on_screen_controller_view.decreaseScale() }), Pair(R.drawable.ic_zoom_out, { binding.onScreenControllerView.decreaseScale() }),
Pair(R.drawable.ic_zoom_in, { on_screen_controller_view.increaseScale() }), Pair(R.drawable.ic_zoom_in, { binding.onScreenControllerView.increaseScale() }),
Pair(R.drawable.ic_close, closeAction) Pair(R.drawable.ic_close, closeAction)
) )
@ -82,11 +84,11 @@ class OnScreenEditActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState : Bundle?) { override fun onCreate(savedInstanceState : Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.on_screen_edit_activity) setContentView(binding.root)
on_screen_controller_view.recenterSticks = Settings(this).onScreenControlRecenterSticks binding.onScreenControllerView.recenterSticks = Settings(this).onScreenControlRecenterSticks
actions.forEach { pair -> actions.forEach { pair ->
fab_parent.addView(LayoutInflater.from(this).inflate(R.layout.on_screen_edit_mini_fab, fab_parent, false).apply { binding.fabParent.addView(LayoutInflater.from(this).inflate(R.layout.on_screen_edit_mini_fab, binding.fabParent, false).apply {
(this as FloatingActionButton).setImageDrawable(ContextCompat.getDrawable(context, pair.first)) (this as FloatingActionButton).setImageDrawable(ContextCompat.getDrawable(context, pair.first))
setOnClickListener { pair.second.invoke() } setOnClickListener { pair.second.invoke() }
fabMapping[pair.first] = this fabMapping[pair.first] = this

View File

@ -12,8 +12,8 @@ import android.util.AttributeSet
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.Preference.SummaryProvider import androidx.preference.Preference.SummaryProvider
import emu.skyline.R import emu.skyline.R
import emu.skyline.di.InputManagerProviderEntryPoint
import emu.skyline.input.ControllerActivity import emu.skyline.input.ControllerActivity
import emu.skyline.input.InputManager
/** /**
* This preference is used to launch [ControllerActivity] using a preference * This preference is used to launch [ControllerActivity] using a preference
@ -26,6 +26,8 @@ class ControllerPreference @JvmOverloads constructor(context : Context, attrs :
override var requestCode = 0 override var requestCode = 0
private val inputManager = InputManagerProviderEntryPoint.getInputManager(context)
init { init {
for (i in 0 until attrs!!.attributeCount) { for (i in 0 until attrs!!.attributeCount) {
val attr = attrs.getAttributeName(i) val attr = attrs.getAttributeName(i)
@ -43,7 +45,7 @@ class ControllerPreference @JvmOverloads constructor(context : Context, attrs :
key = "controller_$index" key = "controller_$index"
title = "${context.getString(R.string.config_controller)} #${index + 1}" title = "${context.getString(R.string.config_controller)} #${index + 1}"
summaryProvider = SummaryProvider<ControllerPreference> { InputManager.controllers[index]!!.type.stringRes.let { context.getString(it) } } summaryProvider = SummaryProvider<ControllerPreference> { inputManager.controllers[index]!!.type.stringRes.let { context.getString(it) } }
} }
/** /**
@ -55,7 +57,7 @@ class ControllerPreference @JvmOverloads constructor(context : Context, attrs :
override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) { override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
if (this.requestCode == requestCode) { if (this.requestCode == requestCode) {
InputManager.syncObjects() inputManager.syncObjects()
notifyChanged() notifyChanged()
} }
} }

View File

@ -15,8 +15,8 @@ import emu.skyline.R
/** /**
* This class adapts [EditTextPreference] so that it supports setting the value as the summary automatically. Also added useful attributes. * This class adapts [EditTextPreference] so that it supports setting the value as the summary automatically. Also added useful attributes.
*/ */
class CustomEditTextPreference : EditTextPreference { class CustomEditTextPreference @JvmOverloads constructor(context : Context, attrs : AttributeSet? = null, defStyleAttr : Int = androidx.preference.R.attr.editTextPreferenceStyle) : EditTextPreference(context, attrs, defStyleAttr) {
constructor(context : Context, attrs : AttributeSet?, defStyleAttr : Int, defStyleRes : Int) : super(context, attrs, defStyleAttr, defStyleRes) { init {
attrs?.let { attrs?.let {
val a = context.obtainStyledAttributes(it, R.styleable.CustomEditTextPreference, defStyleAttr, 0) val a = context.obtainStyledAttributes(it, R.styleable.CustomEditTextPreference, defStyleAttr, 0)
val limit = a.getInt(R.styleable.CustomEditTextPreference_limit, -1) val limit = a.getInt(R.styleable.CustomEditTextPreference_limit, -1)
@ -33,12 +33,6 @@ class CustomEditTextPreference : EditTextPreference {
} }
} }
constructor(context : Context, attrs : AttributeSet?, defStyleAttr : Int) : this(context, attrs, defStyleAttr, 0)
constructor(context : Context, attrs : AttributeSet?) : this(context, attrs, androidx.preference.R.attr.editTextPreferenceStyle)
constructor(context : Context) : this(context, null)
override fun onAttached() { override fun onAttached() {
super.onAttached() super.onAttached()

View File

@ -16,7 +16,6 @@ import com.google.android.material.snackbar.Snackbar
import emu.skyline.KeyReader import emu.skyline.KeyReader
import emu.skyline.R import emu.skyline.R
import emu.skyline.SettingsActivity import emu.skyline.SettingsActivity
import kotlinx.android.synthetic.main.settings_activity.*
/** /**
* Launches [FileActivity] and process the selected file for key import * Launches [FileActivity] and process the selected file for key import
@ -27,14 +26,14 @@ class FilePreference @JvmOverloads constructor(context : Context?, attrs : Attri
override fun onClick() = (context as Activity).startActivityForResult(Intent(context, FileActivity::class.java).apply { putExtra(DocumentActivity.KEY_NAME, key) }, requestCode) override fun onClick() = (context as Activity).startActivityForResult(Intent(context, FileActivity::class.java).apply { putExtra(DocumentActivity.KEY_NAME, key) }, requestCode)
override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) { override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
if (this.requestCode == requestCode) { if (this.requestCode == requestCode && requestCode == Activity.RESULT_OK) {
if (key == "prod_keys" || key == "title_keys") { if (key == "prod_keys" || key == "title_keys") {
val success = KeyReader.import( val success = KeyReader.import(
context, context,
Uri.parse(PreferenceManager.getDefaultSharedPreferences(context).getString(key, "")), Uri.parse(PreferenceManager.getDefaultSharedPreferences(context).getString(key, "")),
KeyReader.KeyType.parse(key) KeyReader.KeyType.parse(key)
) )
Snackbar.make((context as SettingsActivity).settings, if (success) R.string.import_keys_success else R.string.import_keys_failed, Snackbar.LENGTH_LONG).show() Snackbar.make((context as SettingsActivity).binding.root, if (success) R.string.import_keys_success else R.string.import_keys_failed, Snackbar.LENGTH_LONG).show()
} }
} }
} }

View File

@ -9,37 +9,34 @@ import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.view.* import android.view.*
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import emu.skyline.R import emu.skyline.databinding.LicenseDialogBinding
import kotlinx.android.synthetic.main.license_dialog.*
/** /**
* This dialog is used to display the contents of a license for a particular project * This dialog is used to display the contents of a license for a particular project
*/ */
class LicenseDialog : DialogFragment() { class LicenseDialog : DialogFragment() {
private lateinit var binding : LicenseDialogBinding
/** /**
* This inflates the layout of the dialog and sets the minimum width/height to 90% of the screen size * This inflates the layout of the dialog and sets the minimum width/height to 90% of the screen size
*/ */
override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) : View? { override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) : View? {
val layout = layoutInflater.inflate(R.layout.license_dialog, container)
val displayRectangle = Rect() val displayRectangle = Rect()
val window : Window = requireActivity().window val window : Window = requireActivity().window
window.decorView.getWindowVisibleDisplayFrame(displayRectangle) window.decorView.getWindowVisibleDisplayFrame(displayRectangle)
layout.minimumWidth = ((displayRectangle.width() * 0.9f).toInt()) return LicenseDialogBinding.inflate(inflater).apply {
layout.minimumHeight = ((displayRectangle.height() * 0.9f).toInt()) root.minimumWidth = ((displayRectangle.width() * 0.9f).toInt())
root.minimumHeight = ((displayRectangle.height() * 0.9f).toInt())
return layout binding = this
}.root
} }
/**
* This sets the [license_url] and [license_content] based on arguments passed
*/
override fun onActivityCreated(savedInstanceState : Bundle?) { override fun onActivityCreated(savedInstanceState : Bundle?) {
super.onActivityCreated(savedInstanceState) super.onActivityCreated(savedInstanceState)
license_url.text = arguments?.getString("libraryUrl")!! binding.licenseUrl.text = requireArguments().getString("libraryUrl")
license_content.text = context?.getString(arguments?.getInt("libraryLicense")!!)!! binding.licenseContent.text = getString(requireArguments().getInt("libraryLicense"))
dialog?.setOnKeyListener { _, keyCode, event -> dialog?.setOnKeyListener { _, keyCode, event ->
if (keyCode == KeyEvent.KEYCODE_BUTTON_B && event.action == KeyEvent.ACTION_UP) { if (keyCode == KeyEvent.KEYCODE_BUTTON_B && event.action == KeyEvent.ACTION_UP) {

View File

@ -16,63 +16,50 @@ import emu.skyline.R
/** /**
* This preference is used to show licenses and the source of a library * This preference is used to show licenses and the source of a library
*/ */
class LicensePreference : Preference { class LicensePreference @JvmOverloads constructor(context : Context?, attrs : AttributeSet? = null, defStyleAttr : Int = R.attr.dialogPreferenceStyle) : Preference(context, attrs, defStyleAttr) {
/** /**
* The [FragmentManager] is used to show the [LicenseDialog] fragment * The [FragmentManager] is used to show the [LicenseDialog] fragment
*/ */
private val fragmentManager : FragmentManager private val fragmentManager = (context as AppCompatActivity).supportFragmentManager
/** companion object {
* The tag used by this preference when launching a corresponding fragment private const val LIBRARY_URL_ARG = "libraryUrl"
*/ private const val LIBRARY_LICENSE_ARG = "libraryLicense"
private val mDialogFragmentTag = "LicensePreference"
private val DIALOG_TAG = LicensePreference::class.java.simpleName
}
/** /**
* The URL of the library * The URL of the library
*/ */
private var libraryUrl : String? = null private lateinit var libraryUrl : String
/** /**
* The contents of the license of this library * The contents of the license of this library
*/ */
private var libraryLicense : Int? = null private var libraryLicense = 0
/**
* The constructor assigns the [fragmentManager] from the activity and finds [libraryUrl] and [libraryLicense] in the attributes
*/
constructor(context : Context?, attrs : AttributeSet?, defStyleAttr : Int, defStyleRes : Int) : super(context, attrs, defStyleAttr, defStyleRes) {
fragmentManager = (context as AppCompatActivity).supportFragmentManager
init {
for (i in 0 until attrs!!.attributeCount) { for (i in 0 until attrs!!.attributeCount) {
val attr = attrs.getAttributeName(i) when (attrs.getAttributeName(i)) {
LIBRARY_URL_ARG -> libraryUrl = attrs.getAttributeValue(i)
if (attr.equals("libraryUrl", ignoreCase = true)) LIBRARY_LICENSE_ARG -> libraryLicense = attrs.getAttributeValue(i).substring(1).toInt()
libraryUrl = attrs.getAttributeValue(i) }
else if (attr.equals("libraryLicense", ignoreCase = true))
libraryLicense = attrs.getAttributeValue(i).substring(1).toInt()
} }
} }
constructor(context : Context?, attrs : AttributeSet?, defStyleAttr : Int) : this(context, attrs, defStyleAttr, 0)
constructor(context : Context?, attrs : AttributeSet?) : this(context, attrs, R.attr.dialogPreferenceStyle)
constructor(context : Context?) : this(context, null)
/** /**
* The [LicenseDialog] fragment is shown using [fragmentManager] on click with [libraryUrl] and [libraryLicense] passed as arguments * The [LicenseDialog] fragment is shown using [fragmentManager] on click with [libraryUrl] and [libraryLicense] passed as arguments
*/ */
override fun onClick() { override fun onClick() {
if (fragmentManager.findFragmentByTag(mDialogFragmentTag) != null) fragmentManager.findFragmentByTag(DIALOG_TAG) ?: run {
return LicenseDialog().apply {
arguments = Bundle().apply {
val dialog = LicenseDialog() putString(LIBRARY_URL_ARG, libraryUrl)
putInt(LIBRARY_LICENSE_ARG, libraryLicense)
val bundle = Bundle(2) }
bundle.putString("libraryUrl", libraryUrl!!) }.show(fragmentManager, DIALOG_TAG)
bundle.putInt("libraryLicense", libraryLicense!!) }
dialog.arguments = bundle
dialog.show(fragmentManager, mDialogFragmentTag)
} }
} }

View File

@ -9,17 +9,12 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.R
/** /**
* This preference is used to set the theme to Light/Dark mode * This preference is used to set the theme to Light/Dark mode
*/ */
class ThemePreference : ListPreference { class ThemePreference @JvmOverloads constructor(context : Context?, attrs : AttributeSet? = null, defStyleAttr : Int = R.attr.dialogPreferenceStyle) : ListPreference(context, attrs, defStyleAttr) {
constructor(context : Context?, attrs : AttributeSet?, defStyleAttr : Int) : super(context, attrs, defStyleAttr)
constructor(context : Context?, attrs : AttributeSet?) : super(context, attrs)
constructor(context : Context?) : super(context)
/** /**
* This changes [AppCompatDelegate.sDefaultNightMode] based on what the user's selection is * This changes [AppCompatDelegate.sDefaultNightMode] based on what the user's selection is
*/ */

View File

@ -16,7 +16,7 @@ inline fun <reified T> sharedPreferences(context : Context, default : T, prefix
class SharedPreferencesDelegate<T>(context : Context, private val clazz : Class<T>, private val default : T, private val prefix : String, prefName : String?) : ReadWriteProperty<Any, T> { class SharedPreferencesDelegate<T>(context : Context, private val clazz : Class<T>, private val default : T, private val prefix : String, prefName : String?) : ReadWriteProperty<Any, T> {
private val prefs = prefName?.let { context.getSharedPreferences(prefName, Context.MODE_PRIVATE) } ?: PreferenceManager.getDefaultSharedPreferences(context) private val prefs = prefName?.let { context.getSharedPreferences(prefName, Context.MODE_PRIVATE) } ?: PreferenceManager.getDefaultSharedPreferences(context)
override fun setValue(thisRef : Any, property : KProperty<*>, value : T) = (prefix + pascalToSnakeCase(property.name)).let { keyName -> override fun setValue(thisRef : Any, property : KProperty<*>, value : T) = (prefix + camelToSnakeCase(property.name)).let { keyName ->
prefs.edit().apply { prefs.edit().apply {
when (clazz) { when (clazz) {
Float::class.java, java.lang.Float::class.java -> putFloat(keyName, value as Float) Float::class.java, java.lang.Float::class.java -> putFloat(keyName, value as Float)
@ -27,7 +27,7 @@ class SharedPreferencesDelegate<T>(context : Context, private val clazz : Class<
}.apply() }.apply()
} }
override fun getValue(thisRef : Any, property : KProperty<*>) : T = (prefix + pascalToSnakeCase(property.name)).let { keyName -> override fun getValue(thisRef : Any, property : KProperty<*>) : T = (prefix + camelToSnakeCase(property.name)).let { keyName ->
prefs.let { prefs.let {
@Suppress("IMPLICIT_CAST_TO_ANY") @Suppress("IMPLICIT_CAST_TO_ANY")
when (clazz) { when (clazz) {
@ -39,7 +39,7 @@ class SharedPreferencesDelegate<T>(context : Context, private val clazz : Class<
} as T } as T
} }
private fun pascalToSnakeCase(text : String) = StringBuilder().apply { private fun camelToSnakeCase(text : String) = StringBuilder().apply {
text.forEachIndexed { index, c -> text.forEachIndexed { index, c ->
if (index != 0 && c.isUpperCase()) append('_') if (index != 0 && c.isUpperCase()) append('_')
append(c.toLowerCase()) append(c.toLowerCase())

View File

@ -11,13 +11,13 @@ import androidx.core.view.MarginLayoutParamsCompat
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import emu.skyline.R import emu.skyline.databinding.ViewSearchBarBinding
import kotlinx.android.synthetic.main.view_search_bar.view.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
class SearchBarView @JvmOverloads constructor(context : Context, attrs : AttributeSet? = null, defStyleAttr : Int = com.google.android.material.R.attr.materialCardViewStyle) : MaterialCardView(context, attrs, defStyleAttr) { class SearchBarView @JvmOverloads constructor(context : Context, attrs : AttributeSet? = null, defStyleAttr : Int = com.google.android.material.R.attr.materialCardViewStyle) : MaterialCardView(context, attrs, defStyleAttr) {
private val binding = ViewSearchBarBinding.inflate(LayoutInflater.from(context), this)
init { init {
LayoutInflater.from(context).inflate(R.layout.view_search_bar, this)
useCompatPadding = true useCompatPadding = true
} }
@ -32,32 +32,32 @@ class SearchBarView @JvmOverloads constructor(context : Context, attrs : Attribu
cardElevation = radius / 2f cardElevation = radius / 2f
} }
fun setRefreshIconListener(listener : OnClickListener) = refresh_icon.setOnClickListener(listener) fun setRefreshIconListener(listener : OnClickListener) = binding.refreshIcon.setOnClickListener(listener)
fun setLogIconListener(listener : OnClickListener) = log_icon.setOnClickListener(listener) fun setLogIconListener(listener : OnClickListener) = binding.logIcon.setOnClickListener(listener)
fun setSettingsIconListener(listener : OnClickListener) = settings_icon.setOnClickListener(listener) fun setSettingsIconListener(listener : OnClickListener) = binding.settingsIcon.setOnClickListener(listener)
var refreshIconVisible = false var refreshIconVisible = false
set(visible) { set(visible) {
field = visible field = visible
refresh_icon.apply { binding.refreshIcon.apply {
if (visible != isVisible) { if (visible != isVisible) {
refresh_icon.alpha = if (visible) 0f else 1f binding.refreshIcon.alpha = if (visible) 0f else 1f
animate().alpha(if (visible) 1f else 0f).withStartAction { isVisible = true }.withEndAction { isInvisible = !visible }.apply { duration = 500 }.start() animate().alpha(if (visible) 1f else 0f).withStartAction { isVisible = true }.withEndAction { isInvisible = !visible }.apply { duration = 500 }.start()
} }
} }
} }
var text : CharSequence var text : CharSequence
get() = search_field.text get() = binding.searchField.text
set(value) = search_field.setText(value) set(value) = binding.searchField.setText(value)
fun startTitleAnimation() { fun startTitleAnimation() {
motion_layout.progress = 0f binding.motionLayout.progress = 0f
motion_layout.transitionToEnd() binding.motionLayout.transitionToEnd()
search_field.apply { binding.searchField.apply {
setOnFocusChangeListener { v, hasFocus -> setOnFocusChangeListener { v, hasFocus ->
if (hasFocus) { if (hasFocus) {
this@SearchBarView.motion_layout.progress = 1f binding.motionLayout.progress = 1f
context.getSystemService(InputMethodManager::class.java).showSoftInput(v, InputMethodManager.SHOW_IMPLICIT) context.getSystemService(InputMethodManager::class.java).showSoftInput(v, InputMethodManager.SHOW_IMPLICIT)
onFocusChangeListener = null onFocusChangeListener = null
} }
@ -66,23 +66,23 @@ class SearchBarView @JvmOverloads constructor(context : Context, attrs : Attribu
} }
fun animateRefreshIcon() { fun animateRefreshIcon() {
refresh_icon.animate().rotationBy(-180f) binding.refreshIcon.animate().rotationBy(-180f)
} }
inline fun addTextChangedListener( fun addTextChangedListener(
crossinline beforeTextChanged : ( beforeTextChanged : (
text : CharSequence?, text : CharSequence?,
start : Int, start : Int,
count : Int, count : Int,
after : Int after : Int
) -> Unit = { _, _, _, _ -> }, ) -> Unit = { _, _, _, _ -> },
crossinline onTextChanged : ( onTextChanged : (
text : CharSequence?, text : CharSequence?,
start : Int, start : Int,
before : Int, before : Int,
count : Int count : Int
) -> Unit = { _, _, _, _ -> }, ) -> Unit = { _, _, _, _ -> },
crossinline afterTextChanged : (text : Editable?) -> Unit = {} afterTextChanged : (text : Editable?) -> Unit = {}
) : TextWatcher { ) : TextWatcher {
val textWatcher = object : TextWatcher { val textWatcher = object : TextWatcher {
override fun afterTextChanged(s : Editable?) { override fun afterTextChanged(s : Editable?) {
@ -97,7 +97,7 @@ class SearchBarView @JvmOverloads constructor(context : Context, attrs : Attribu
onTextChanged.invoke(text, start, before, count) onTextChanged.invoke(text, start, before, count)
} }
} }
search_field.addTextChangedListener(textWatcher) binding.searchField.addTextChangedListener(textWatcher)
return textWatcher return textWatcher
} }

View File

@ -1,16 +1,18 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".input.ControllerActivity"> tools:context=".input.ControllerActivity">
<include layout="@layout/titlebar" /> <include
android:id="@+id/titlebar"
layout="@layout/titlebar" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/controller_list" android:id="@+id/controller_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" /> app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,20 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".LogActivity"> tools:context=".LogActivity">
<include layout="@layout/titlebar" /> <include
android:id="@+id/titlebar"
layout="@layout/titlebar" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/log_list" android:id="@+id/log_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fastScrollEnabled="true" android:fastScrollEnabled="true"
android:focusedByDefault="true" android:focusedByDefault="true"
android:transcriptMode="normal" android:transcriptMode="normal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="@string/appbar_scrolling_view_behavior" /> app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android" <TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text_title" android:id="@+id/text_title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:textSize="18sp" android:textSize="18sp"
android:textStyle="bold" /> android:textStyle="bold" />

View File

@ -1,14 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="wrap_content" android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<include layout="@layout/titlebar" /> <include
android:id="@+id/titlebar"
layout="@layout/titlebar" />
<FrameLayout <FrameLayout
android:id="@+id/settings" android:id="@+id/settings"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" /> app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.appbar.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.appbar.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/app_bar_layout" android:id="@+id/app_bar_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
android:keyboardNavigationCluster="false" android:keyboardNavigationCluster="false"
android:touchscreenBlocksFocus="false"> android:touchscreenBlocksFocus="false">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
app:elevation="16dp" app:elevation="16dp"
app:layout_scrollFlags="scroll" /> app:layout_scrollFlags="scroll" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>

View File

@ -1,24 +1,25 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.4.30' ext.kotlin_version = '1.4.21'
ext.lifecycle_version = '2.2.0'
ext.hilt_version = '2.31.2-alpha'
repositories { repositories {
google() google()
jcenter() jcenter()
mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.1.2' classpath 'com.android.tools.build:gradle:4.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
} }
} }
plugins {
id "com.github.ben-manes.versions" version "0.36.0"
}
allprojects { allprojects {
repositories { repositories {
google() google()