Skip to content

Commit

Permalink
Merge pull request #20 from steamclock/jm/improvements
Browse files Browse the repository at this point in the history
[18, 19] Add new text option for menu and add ability to generate selections via annotated function
  • Loading branch information
jacobminer authored Mar 23, 2022
2 parents 5b42608 + 1bc47ad commit 6e55d69
Show file tree
Hide file tree
Showing 14 changed files with 245 additions and 42 deletions.
64 changes: 55 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ class App: Application() {
super.onCreate()
DebugMenu.initialize("5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8",
ComposeDebugMenuDisplay(this),
SharedPrefsPersistence(this)
SharedPrefsPersistence(this),
header = "", // optional header to display on all debug menus
footer = "" // optional footer to display on all debug menus
)
}
}
Expand All @@ -84,6 +86,7 @@ Currently the following options are supported:
| `LongValue` | An input field for long values | `0L` |
| `Action` | A button that will call the code provided | n/a |
| `OptionSelection` | A drop-down menu of string values | `null` |
| `TextDisplay` | A string, useful for showing information | n/a |

## Manual Usage
### Adding Options
Expand Down Expand Up @@ -119,6 +122,9 @@ class MainActivity : AppCompatActivity() {
key = "test-selection",
options = listOf("value1", "value2"),
defaultIndex = null
),
TextDisplay(
text = "Test display text"
)
)
}
Expand Down Expand Up @@ -186,13 +192,15 @@ Because the all annotated menu options are added at compile time, it's recommend
Adding options can be done using the following Annotations:

| Annotation | `BooleanValue` | UI Shown |
|-------------------|-------------------|-------------------------------------------|
| `@DebugBoolean` | `BooleanValue` | A true/false toggle |
| `@DebugInt` | `IntValue` | An input field for integer values |
| `@DebugDouble` | `DoubleValue` | An input field for double values |
| `@DebugLong` | `LongValue` | An input field for long values |
| `@DebugAction` | `Action` | A button that will call the code provided |
| `@DebugSelection` | `OptionSelection` | A drop-down menu of string values |
|----------------------|-------------------|-------------------------------------------|
| `@DebugBoolean` | `BooleanValue` | A true/false toggle |
| `@DebugInt` | `IntValue` | An input field for integer values |
| `@DebugDouble` | `DoubleValue` | An input field for double values |
| `@DebugLong` | `LongValue` | An input field for long values |
| `@DebugAction` | `Action` | A button that will call the code provided |
| `@DebugSelection` | `OptionSelection` | A drop-down menu of string values |
| `@DebugTextProvider` | `TextDisplay` | A string, useful for showing information |
| `@DebugSelectionProvider` | `OptionSelection` | A drop-down menu of string values |

All annotations support optional `defaultValues` and `menuKey` parameters.

Expand Down Expand Up @@ -220,7 +228,45 @@ It's also possible to define an action scoped to a class using `@DebugAction`.
class MainActivity : AppCompatActivity() {
@DebugAction(title = "Scoped action")
fun scopedGlobalAction() {
// can access MainActivity's state
// can access MainActivity's state, is called when the onClick for the debug button is invoked
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

initDebugMenus() // generated extension function that must be called for scoped actions to appear
}
}
```

You can also provide a text provider, scoped to a class using `@DebugTextProvider`. `@DebugTextProvider` is currently only available when scoped to a class.
```kotlin
class MainActivity : AppCompatActivity() {
@DebugTextProvider
fun textProvider(): String {
// string generated when `initDebugMenus()` is called
return "Simple text provider"
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

initDebugMenus() // generated extension function that must be called for scoped actions to appear
}
}
```


You can also define an option selection provider, scoped to a class using `@DebugSelectionProvider`. `@DebugSelectionProvider` is currently only available when scoped to a class.
```kotlin
class MainActivity : AppCompatActivity() {
@DebugSelectionProvider
fun textProvider(): List<String> {
// list of options generated when `initDebugMenus()` is called
return listOf(
"Option 1",
"Option 2",
)
}

override fun onCreate(savedInstanceState: Bundle?) {
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/java/com/steamclock/debugmenusample/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ class App: Application() {
// 123321
DebugMenu.initialize("a320480f534776bddb5cdb54b1e93d210a3c7d199e80a23c1b2178497b184c76",
ComposeDebugMenuDisplay(this),
SharedPrefsPersistence(this)
SharedPrefsPersistence(this),
header = "Testing header",
footer = "Testing footer"
)
}
}
14 changes: 14 additions & 0 deletions app/src/main/java/com/steamclock/debugmenusample/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ class MainActivity : AppCompatActivity() {
TestingMenu.show()
}

@DebugTextProvider(menuKey = Debug.TestingMenu)
fun temp(): String {
return "This is testing text"
}

@DebugSelectionProvider(title = "Annotated Selection")
fun temp2(): List<String> {
return listOf(
"testing 1",
"testing 2",
2.toString()
)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,14 @@ annotation class DebugDouble(val title: String, val defaultValue: Double = 0.0,
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class DebugSelection(val title: String, val defaultIndex: Int = -1, val menuKey: String = "", val options: Array<String>)

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class DebugTextProvider(val menuKey: String = "")

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class DebugSelectionProvider(val title: String, val defaultIndex: Int = -1, val menuKey: String = "")

Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
package com.steamclock.codegen

import com.steamclock.debugmenu.DebugMenu
import com.steamclock.debugmenu.OptionSelection
import com.steamclock.debugmenu.flow
import com.steamclock.debugmenu_annotation.DebugValue
import kotlinx.coroutines.flow.mapNotNull

internal class MenuClassBuilder(
private val menuKey: String,
private val menuName: String,
Expand Down Expand Up @@ -54,6 +48,12 @@ internal class MenuClassBuilder(
"OptionSelection(title = \"${selection.title}\", key = \"${selection.key}\", options = listOf(${options}), defaultIndex = ${selection.defaultIndex})"
"DebugMenu.instance.addOptions(key, $text)"
}
is TextValueWrapper -> {
""
}
is SelectionProviderWrapper -> {
""
}
}
}

Expand All @@ -63,6 +63,12 @@ internal class MenuClassBuilder(
is ActionWrapper -> {
""
}
is TextValueWrapper -> {
""
}
is SelectionProviderWrapper -> {
""
}
is BooleanWrapper -> {
val toggle = it.toggle
"val ${toggle.key} = DebugValue<Boolean>(DebugMenu.instance.flow(\"${toggle.key}\"))"
Expand Down Expand Up @@ -106,12 +112,33 @@ internal class MenuClassBuilder(
.groupBy { it.parentClass }
}

private val referencedParents: Set<String>
private val textProviderByParents: Map<String, List<TextValueWrapper>>
get() {
return options
.filterIsInstance<TextValueWrapper>()
.groupBy { it.parentClass }
}

private val selectionProviderByParents: Map<String, List<SelectionProviderWrapper>>
get() {
return options
.filterIsInstance<SelectionProviderWrapper>()
.groupBy { it.parentClass }
}

private val referencedParents: Set<String>
get() {
val actions = options
.filterIsInstance<ActionWrapper>()
.filter { !it.isGlobal }
.map { it.parentClass }.toSet()
.map { it.parentClass }
val textProviders = options
.filterIsInstance<TextValueWrapper>()
.map { it.parentClass }
val selectionProviders = options
.filterIsInstance<SelectionProviderWrapper>()
.map { it.parentClass }
return (actions + textProviders + selectionProviders).toSet()
}

private val weakReferences: String
Expand All @@ -128,9 +155,11 @@ internal class MenuClassBuilder(

private val initFunctions: String
get() {
return actionsByParents.keys.joinToString(separator = "\n ") { parent ->
val actions = actionsByParents[parent] ?: listOf()
return referencedParents.joinToString(separator = "\n ") { parent ->
val parentName = parent.split(".").last()
val actions = actionsByParents[parent] ?: listOf()
val textProviders = textProviderByParents[parent] ?: listOf()
val selectionProviders = selectionProviderByParents[parent] ?: listOf()

val actionStrings = actions.joinToString("") {
"""
Expand All @@ -141,12 +170,43 @@ internal class MenuClassBuilder(
"""
}

"""
val textProviderStrings = textProviders.joinToString("") {
"""
val ${it.functionName}Text = instance.${parentName}Ref?.get()?.${it.functionName}() as? Any
if (${it.functionName}Text !is String) {
throw Exception("${parent}.${it.functionName} is marked as a StringValueProvider, it must return a String value")
}
val ${it.functionName}TextProvider = TextDisplay(text = ${it.functionName}Text)
DebugMenu.instance.addOptions(key, ${it.functionName}TextProvider)
"""
}

val selectionProviderStrings = selectionProviders.joinToString("") {
"""
val ${it.functionName}Selections = instance.${parentName}Ref?.get()?.${it.functionName}() as? Any
if (${it.functionName}Selections !is List<*>) {
throw Exception("${parent}.${it.functionName} is marked as a DebugSelectionProvider, it must return a List<String> value")
}
${it.functionName}Selections.forEach {
if (it !is String) {
throw Exception("${parent}.${it.functionName} is marked as a DebugSelectionProvider, it must return a List<String> value")
}
}
val ${it.functionName}Selection = OptionSelection(title = "${it.title}", key = "${it.key}", options = ${it.functionName}Selections as List<String>, defaultIndex = ${it.defaultIndex})
DebugMenu.instance.addOptions(key, ${it.functionName}Selection)
"""
}

"""
@Suppress("UNCHECKED_CAST")
fun initialize(parent: $parentName) = runBlocking {
instance.${parentName}Ref = WeakReference(parent)
$actionStrings
}
"""
$textProviderStrings
$selectionProviderStrings
}
"""
}
}

Expand Down Expand Up @@ -190,6 +250,6 @@ class $menuName private constructor() {

fun generatedInitFunctions(): Pair<String, List<String>> {
// DebugMenu -> MainActivity, SettingsActivity
return menuName to actionsByParents.keys.map { parent -> parent }
return menuName to referencedParents.map { parent -> parent }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ internal data class DoubleWrapper(val doubleValue: DoubleValue): AnnotationWrapp
internal data class LongWrapper(val longValue: LongValue): AnnotationWrapper()
internal data class SelectionWrapper(val selectionValue: OptionSelection): AnnotationWrapper()
internal data class ActionWrapper(val title: String, val functionName: String, val parentClass: String, val packageName: String, val isGlobal: Boolean): AnnotationWrapper()
internal data class TextValueWrapper(val functionName: String, val parentClass: String, val packageName: String): AnnotationWrapper()
internal data class SelectionProviderWrapper(val title: String, val key: String, val defaultIndex: Int? = null, val functionName: String, val parentClass: String, val packageName: String): AnnotationWrapper()

@AutoService(Processor::class) // For registering the service
@SupportedSourceVersion(SourceVersion.RELEASE_8) // to support Java 8
Expand Down Expand Up @@ -53,7 +55,9 @@ class FileGenerator : AbstractProcessor() {
DebugDouble::class.java.name,
DebugLong::class.java.name,
DebugAction::class.java.name,
DebugSelection::class.java.name
DebugSelection::class.java.name,
DebugTextProvider::class.java.name,
DebugSelectionProvider::class.java.name
)
}

Expand Down Expand Up @@ -147,6 +151,33 @@ class FileGenerator : AbstractProcessor() {
}
if (result == false) return false

result = roundEnvironment?.forEach(DebugTextProvider::class.java, validKind = ElementKind.METHOD) { element, annotation ->
val menuKey = annotation.menuKey
val functionName = element.simpleName.toString()
val parentClass = element.enclosingElement.toString()
val packageName = parentClass.split(".").dropLast(1).joinToString(".")
addOptionToMenu(menuKey, TextValueWrapper(functionName = functionName, parentClass = parentClass, packageName = packageName))
}
if (result == false) return false

result = roundEnvironment?.forEach(DebugSelectionProvider::class.java, validKind = ElementKind.METHOD) { element, annotation ->
val menuKey = annotation.menuKey
val functionName = element.simpleName.toString()
val parentClass = element.enclosingElement.toString()
val packageName = parentClass.split(".").dropLast(1).joinToString(".")
val name = element.simpleName.toString()
val title = annotation.title
val defaultValue = annotation.defaultIndex

// annotations can't include null, so we use -1 instead to represent the same state
val correctedDefaultValue = if (defaultValue == -1) null else defaultValue

addOptionToMenu(menuKey, SelectionProviderWrapper(
title = title, key = name, defaultIndex = correctedDefaultValue,
functionName = functionName, parentClass = parentClass, packageName = packageName))
}
if (result == false) return false

generateMenuClasses(menus)

return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class DebugMenu private constructor(private val code: String) {
state.display.displayCodeEntry()
return
}
state.display.displayMenu(state.title, state.options[menu]!!)
state.display.displayMenu(state, menu)
}

fun optionForKey(key: String): DebugOption? {
Expand All @@ -97,11 +97,13 @@ class DebugMenu private constructor(private val code: String) {
throw RuntimeException("Call initialize before usage")
}

fun initialize(encryptedCode: String, display: DebugMenuDisplay, persistence: DebugMenuPersistence) {
fun initialize(encryptedCode: String, display: DebugMenuDisplay, persistence: DebugMenuPersistence, header: String? = null, footer: String? = null) {
val previousState = _instance?.state ?: DebugMenuState(title = "Debug Menu")
val newState = previousState.copy(
display = display,
persistence = persistence
persistence = persistence,
header = header,
footer = footer
)
_instance = DebugMenu(encryptedCode)
_instance?.state = newState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import com.steamclock.debugmenu.persistence.DebugMenuPersistence
import com.steamclock.debugmenu.persistence.InMemoryPersistence

data class DebugMenuState(val title: String,
val header: String? = null,
val footer: String? = null,
val options: Map<String, List<DebugOption>> = mapOf(),
val persistence: DebugMenuPersistence = InMemoryPersistence(),
val display: DebugMenuDisplay = LogDisplay())
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ data class IntValue(override val title: String, override val key: String, val de
data class DoubleValue(override val title: String, override val key: String, val defaultValue: Double = 0.0): DebugOption(title, key)
data class LongValue(override val title: String, override val key: String, val defaultValue: Long = 0L): DebugOption(title, key)
data class Action(override val title: String, val onClick: suspend () -> Unit): DebugOption(title, title)
data class OptionSelection(override val title: String, override val key: String, val options: List<String>, val defaultIndex: Int? = null): DebugOption(title, key)
data class OptionSelection(override val title: String, override val key: String, val options: List<String>, val defaultIndex: Int? = null): DebugOption(title, key)
data class TextDisplay(val text: String): DebugOption(text, "")
Loading

0 comments on commit 6e55d69

Please sign in to comment.