Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abbreviations #1208

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
8 changes: 8 additions & 0 deletions app/src/main/java/com/dessalines/thumbkey/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.dessalines.thumbkey.ui.components.settings.backupandrestore.BackupAnd
import com.dessalines.thumbkey.ui.components.settings.behavior.BehaviorScreen
import com.dessalines.thumbkey.ui.components.settings.lookandfeel.LookAndFeelScreen
import com.dessalines.thumbkey.ui.components.setup.SetupScreen
import com.dessalines.thumbkey.ui.screens.AbbreviationsScreen
import com.dessalines.thumbkey.ui.theme.ThumbkeyTheme
import com.dessalines.thumbkey.utils.ANIMATION_SPEED
import com.dessalines.thumbkey.utils.getImeNames
Expand Down Expand Up @@ -154,6 +155,13 @@ class MainActivity : AppCompatActivity() {
appSettingsViewModel = appSettingsViewModel,
)
}
composable(
route = "abbreviations",
) {
AbbreviationsScreen(
navController = navController,
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.dessalines.thumbkey.db

import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class AbbreviationRepository(
private val abbreviationDao: AbbreviationDao,
) {
val allAbbreviations: LiveData<List<Abbreviation>> = abbreviationDao.getAllAbbreviations()

@WorkerThread
suspend fun insertOrUpdate(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

abbr: String,
expansion: String,
id: Int? = null,
) {
if (id != null) {
abbreviationDao.update(id, abbr, expansion)
} else {
abbreviationDao.insert(abbr, expansion)
}
}

@WorkerThread
suspend fun delete(id: Int) {
abbreviationDao.deleteById(id)
}

@WorkerThread
fun getAbbreviation(abbr: String): Abbreviation? = abbreviationDao.getAbbreviation(abbr)
}

class AbbreviationViewModel(
private val repository: AbbreviationRepository,
) : ViewModel() {
val allAbbreviations: LiveData<List<Abbreviation>> = repository.allAbbreviations

fun insertOrUpdate(
abbr: String,
expansion: String,
id: Int? = null,
) = viewModelScope.launch {
repository.insertOrUpdate(abbr, expansion, id)
}

fun delete(id: Int) =
viewModelScope.launch {
repository.delete(id)
}

fun getAbbreviation(abbr: String): Abbreviation? = repository.getAbbreviation(abbr)
}

class AbbreviationViewModelFactory(
private val repository: AbbreviationRepository,
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(AbbreviationViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return AbbreviationViewModel(repository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
89 changes: 86 additions & 3 deletions app/src/main/java/com/dessalines/thumbkey/db/AppDb.kt
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,34 @@ data class BehaviorUpdate(
val ghostKeysEnabled: Int,
)

@Dao
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might as well put all the abbreviation stuff in one file. So rename the AbbreviationRepository.kt above to Abbreviation, and move all this stuff in there.

interface AbbreviationDao {
@Query("SELECT * FROM Abbreviation")
fun getAllAbbreviations(): LiveData<List<Abbreviation>>

@Query("SELECT * FROM Abbreviation WHERE lower(abbreviation) = lower(:abbr) LIMIT 1")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hrm... I'm not sure what the best way to handle case would be. I spose this is fine, but we should decide first whether we should allow upper case abbreviations at all, in which case it'd be better to add a table constraint.

fun getAbbreviation(abbr: String): Abbreviation?

@Query("SELECT * FROM Abbreviation WHERE lower(abbreviation) = lower(:abbr) LIMIT 1")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This query is duped, so you might as well make the string a const.

suspend fun getAbbreviationAsync(abbr: String): Abbreviation?
Copy link
Owner

@dessalines dessalines Dec 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer if async was the default, and change getAbbreviation to getAbbreviationSync. Seems like you'd never need either of these tho, I'll have to read further down.

EDIT: Actually both of these should be removed. All the abbrevations should only be read once as a list, and passed around as necessary.


@Query("UPDATE Abbreviation SET abbreviation = :newAbbr, expansion = :expansion WHERE id = :id")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need this string, read about room updates. It should be enough to do @Update(entity = Abbreviation::class) . There are even examples in this code.

suspend fun update(
id: Int,
newAbbr: String,
expansion: String,
)

@Query("INSERT INTO Abbreviation (abbreviation, expansion) VALUES (:abbr, :expansion)")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with this insert line. Read through an up to date room tutorial.

suspend fun insert(
abbr: String,
expansion: String,
)

@Query("DELETE FROM Abbreviation WHERE id = :id")
suspend fun deleteById(id: Int)
}

@Dao
interface AppSettingsDao {
@Query("SELECT * FROM AppSettings limit 1")
Expand Down Expand Up @@ -582,14 +610,67 @@ val MIGRATION_15_16 =
}
}

val MIGRATION_16_17 =
object : Migration(16, 17) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS Abbreviation (
abbreviation TEXT PRIMARY KEY NOT NULL,
expansion TEXT NOT NULL
Comment on lines +618 to +620
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

)
""",
)
}
}

@Entity
data class Abbreviation(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This column does not exist on your table. I have no idea how this isn't crashing.

@ColumnInfo(name = "abbreviation") val abbreviation: String,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put the columninfo on a separate line if you would, like the other columns in this codebase.

@ColumnInfo(name = "expansion") val expansion: String,
)

val MIGRATION_17_18 =
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a single PR, so you don't need 2 migrations. Just delete the debug application and start with the table you actually want.

Really confused me because you put the entity in between 2 migration definitions. The entity should probably go near the top of the file.

object : Migration(17, 18) {
override fun migrate(db: SupportSQLiteDatabase) {
// Create a temporary table with the new schema
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS Abbreviation_temp (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
abbreviation TEXT NOT NULL,
expansion TEXT NOT NULL
Comment on lines +640 to +643
Copy link
Owner

@dessalines dessalines Dec 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to keep an integer id column that's fine I spose, but add a unique index for the abbreviation field.

)
""",
)

// Copy data from the old table to the new table
db.execSQL(
"""
INSERT INTO Abbreviation_temp (abbreviation, expansion)
SELECT abbreviation, expansion FROM Abbreviation
""",
)

// Drop the old table
db.execSQL("DROP TABLE Abbreviation")

// Rename the temporary table to the original name
db.execSQL("ALTER TABLE Abbreviation_temp RENAME TO Abbreviation")
}
}

@Database(
version = 16,
entities = [AppSettings::class],
version = 18,
entities = [AppSettings::class, Abbreviation::class],
exportSchema = true,
)
abstract class AppDB : RoomDatabase() {
abstract fun appSettingsDao(): AppSettingsDao

abstract fun abbreviationDao(): AbbreviationDao

companion object {
@Volatile
private var instance: AppDB? = null
Expand Down Expand Up @@ -621,7 +702,9 @@ abstract class AppDB : RoomDatabase() {
MIGRATION_13_14,
MIGRATION_14_15,
MIGRATION_15_16,
)
MIGRATION_16_17,
MIGRATION_17_18,
).fallbackToDestructiveMigration()
// Necessary because it can't insert data on creation
.addCallback(
object : Callback() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import com.dessalines.thumbkey.keyboards.EMOJI_BACK_KEY_ITEM
import com.dessalines.thumbkey.keyboards.KB_EN_THUMBKEY_MAIN
import com.dessalines.thumbkey.keyboards.NUMERIC_KEY_ITEM
import com.dessalines.thumbkey.keyboards.RETURN_KEY_ITEM
import com.dessalines.thumbkey.utils.AbbreviationManager
import com.dessalines.thumbkey.utils.CircularDragAction
import com.dessalines.thumbkey.utils.KeyAction
import com.dessalines.thumbkey.utils.KeyboardLayout
Expand All @@ -72,6 +73,7 @@ import com.dessalines.thumbkey.utils.KeyboardPosition
import com.dessalines.thumbkey.utils.TAG
import com.dessalines.thumbkey.utils.getKeyboardMode
import com.dessalines.thumbkey.utils.keyboardPositionToAlignment
import com.dessalines.thumbkey.utils.performKeyAction
import com.dessalines.thumbkey.utils.toBool
import kotlin.time.TimeMark

Expand All @@ -82,14 +84,15 @@ fun KeyboardScreen(
onChangePosition: ((old: KeyboardPosition) -> KeyboardPosition) -> Unit,
) {
val ctx = LocalContext.current as IMEService
val abbreviationManager = remember { AbbreviationManager(ctx.applicationContext) }

var mode by remember {
val startMode =
getKeyboardMode(
ime = ctx,
autoCapitalize = settings?.autoCapitalize?.toBool() ?: false,
)

Log.d(TAG, "KeyboardScreen: Initial keyboard mode: $startMode")
mutableStateOf(startMode)
}

Expand Down Expand Up @@ -226,9 +229,44 @@ fun KeyboardScreen(
if (soundOnTap) {
audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK, .1f)
}
ctx.currentInputConnection.commitText(
it.emoji,
1,
performKeyAction(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is going on here, this has nothing to do with abbreviations.

action = KeyAction.CommitText(it.emoji),
ime = ctx,
autoCapitalize = autoCapitalize,
keyboardSettings = keyboardDefinition.settings,
onToggleShiftMode = { enable ->
mode =
if (enable) {
KeyboardMode.SHIFTED
} else {
capsLock = false
KeyboardMode.MAIN
}
},
onToggleNumericMode = { enable ->
mode =
if (enable) {
KeyboardMode.NUMERIC
} else {
capsLock = false
KeyboardMode.MAIN
}
},
onToggleEmojiMode = { enable ->
mode = if (enable) KeyboardMode.EMOJI else KeyboardMode.MAIN
},
onToggleCapsLock = { capsLock = !capsLock },
onAutoCapitalize = { enable ->
if (mode !== KeyboardMode.NUMERIC) {
if (enable) {
mode = KeyboardMode.SHIFTED
} else if (!capsLock) {
mode = KeyboardMode.MAIN
}
}
},
onSwitchLanguage = onSwitchLanguage,
onChangePosition = onChangePosition,
)
}
emojiPicker
Expand Down Expand Up @@ -273,30 +311,36 @@ fun KeyboardScreen(
slideSpacebarDeadzoneEnabled = slideSpacebarDeadzoneEnabled,
slideBackspaceDeadzoneEnabled = slideBackspaceDeadzoneEnabled,
onToggleShiftMode = { enable ->
Log.d(TAG, "KeyboardScreen: Toggling shift mode, enable: $enable")
mode =
if (enable) {
KeyboardMode.SHIFTED
} else {
capsLock = false
KeyboardMode.MAIN
}
Log.d(TAG, "KeyboardScreen: New keyboard mode: $mode")
},
onToggleNumericMode = { enable ->
Log.d(TAG, "KeyboardScreen: Toggling numeric mode, enable: $enable")
mode =
if (enable) {
KeyboardMode.NUMERIC
} else {
capsLock = false
KeyboardMode.MAIN
}
Log.d(TAG, "KeyboardScreen: New keyboard mode: $mode")
},
onToggleEmojiMode = { enable ->
Log.d(TAG, "KeyboardScreen: Toggling emoji mode, enable: $enable")
mode =
if (enable) {
KeyboardMode.EMOJI
} else {
KeyboardMode.MAIN
}
Log.d(TAG, "KeyboardScreen: New keyboard mode: $mode")
},
onToggleCapsLock = {
capsLock = !capsLock
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.compose.material.icons.outlined.InstallMobile
import androidx.compose.material.icons.outlined.KeyboardAlt
import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material.icons.outlined.Restore
import androidx.compose.material.icons.outlined.ShortText
import androidx.compose.material.icons.outlined.TouchApp
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
Expand Down Expand Up @@ -153,6 +154,16 @@ fun SettingsScreen(
},
onClick = { navController.navigate("behavior") },
)
Preference(
title = { Text(stringResource(R.string.abbreviations)) },
icon = {
Icon(
imageVector = Icons.Outlined.ShortText,
contentDescription = null,
)
},
onClick = { navController.navigate("abbreviations") },
)
Preference(
title = { Text(stringResource(R.string.backup_and_restore)) },
icon = {
Expand Down
Loading