diff --git a/app/src/androidTestKeyboards/kotlin/be/scri/helpers/KeyboardTest.kt b/app/src/androidTestKeyboards/kotlin/be/scri/helpers/KeyboardTest.kt index 09e31f4f..7a0bdcf3 100644 --- a/app/src/androidTestKeyboards/kotlin/be/scri/helpers/KeyboardTest.kt +++ b/app/src/androidTestKeyboards/kotlin/be/scri/helpers/KeyboardTest.kt @@ -98,7 +98,7 @@ class KeyboardTest { every { mockIME.findWhetherWordIsPlural(any(), "in") } returns false every { mockIME.getCaseAnnotationForPreposition(any(), "in") } returns null - every { mockIME.updateAutoSuggestText(any(), any(), any(), any()) } answers { + every { mockIME.updateAutoSuggestText(any(), any(), any()) } answers { conjugateBtn.text = "der" pluralBtn.text = "den" translateBtn.text = "die" @@ -106,6 +106,8 @@ class KeyboardTest { suggestionHandler.processLinguisticSuggestions("in") + Thread.sleep(100) + verify { conjugateBtn.text = match { it.isNotEmpty() } } verify { pluralBtn.text = match { it.isNotEmpty() } } verify { translateBtn.text = match { it.isNotEmpty() } } diff --git a/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt b/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt index 704ca937..818e8feb 100644 --- a/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt +++ b/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt @@ -50,6 +50,10 @@ class KeyHandler( resetShiftIfNeeded(code) + if (code != KeyboardBase.KEYCODE_SHIFT && code != KeyboardBase.KEYCODE_MODE_CHANGE) { + ime.hideClipboardSuggestionChip() + } + val previousWasLastKeySpace = wasLastKeySpace if (code != KeyboardBase.KEYCODE_SPACE && code != KeyboardBase.KEYCODE_ENTER) { suggestionHandler.clearLinguisticSuggestions() @@ -113,6 +117,10 @@ class KeyHandler( handleCurrencyKey(language) true } + KeyboardBase.KEYCODE_CLIPBOARD -> { + ime.openClipboardPanel() + true + } else -> { handleDefaultKey(code) true diff --git a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt index 102bf598..92e04959 100644 --- a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt +++ b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt @@ -73,6 +73,8 @@ class KeyboardUIManager( fun processLinguisticSuggestions(word: String) fun isNumericKeyboardActive(): Boolean + + fun onClipboardSuggestionClicked() } var keyboardView: KeyboardView = binding.keyboardView @@ -106,8 +108,14 @@ class KeyboardUIManager( } private fun setupClickListeners() { - binding.scribeKeyOptions.setOnClickListener { listener.onScribeKeyOptionsClicked() } - binding.scribeKeyToolbar.setOnClickListener { listener.onScribeKeyToolbarClicked() } + binding.scribeKeyOptions.setOnClickListener { + hideClipboardSuggestionChip() + listener.onScribeKeyOptionsClicked() + } + binding.scribeKeyToolbar.setOnClickListener { + hideClipboardSuggestionChip() + listener.onScribeKeyToolbarClicked() + } binding.translateBtn.setOnClickListener { listener.onTranslateClicked() } binding.conjugateBtn.setOnClickListener { listener.onConjugateClicked() } @@ -115,6 +123,8 @@ class KeyboardUIManager( binding.scribeKeyClose.setOnClickListener { listener.onCloseClicked() } + binding.clipboardSuggestionChip.setOnClickListener { listener.onClipboardSuggestionClicked() } + // Info button listener for INVALID state. binding.ivInfo.setOnClickListener { showInvalidInfo() } } @@ -940,4 +950,53 @@ class KeyboardUIManager( ) } } + + fun showClipboardSuggestionChip(clipText: String) { + val truncatedText = + if (clipText.length > 25) { + clipText.take(22) + "..." + } else { + clipText + } + binding.clipboardSuggestionChip.text = truncatedText + binding.clipboardSuggestionChip.visibility = View.VISIBLE + + binding.translateBtn.visibility = View.GONE + binding.conjugateBtn.visibility = View.GONE + binding.pluralBtn.visibility = View.GONE + binding.separator2.visibility = View.GONE + binding.separator3.visibility = View.GONE + + binding.emojiBtnPhone1?.visibility = View.GONE + binding.emojiBtnPhone2?.visibility = View.GONE + binding.emojiBtnTablet1?.visibility = View.GONE + binding.emojiBtnTablet2?.visibility = View.GONE + binding.emojiBtnTablet3?.visibility = View.GONE + binding.separator4.visibility = View.GONE + binding.separator5.visibility = View.GONE + binding.separator6.visibility = View.GONE + } + + fun hideClipboardSuggestionChip() { + if (binding.clipboardSuggestionChip.visibility == View.VISIBLE) { + binding.clipboardSuggestionChip.visibility = View.GONE + binding.translateBtn.visibility = View.VISIBLE + binding.conjugateBtn.visibility = View.VISIBLE + binding.pluralBtn.visibility = View.VISIBLE + binding.separator2.visibility = View.VISIBLE + binding.separator3.visibility = View.VISIBLE + } + } + + fun showClipboardPanel() { + binding.clipboardPanelHolder.visibility = View.VISIBLE + keyboardView.visibility = View.INVISIBLE + binding.commandOptionsBar.visibility = View.INVISIBLE + } + + fun hideClipboardPanel() { + binding.clipboardPanelHolder.visibility = View.GONE + keyboardView.visibility = View.VISIBLE + binding.commandOptionsBar.visibility = View.VISIBLE + } } diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt index 3e901ee6..4497353a 100644 --- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt +++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt @@ -58,11 +58,13 @@ import be.scri.helpers.SHIFT_OFF import be.scri.helpers.SHIFT_ON_ONE_CHAR import be.scri.helpers.SHIFT_ON_PERMANENT import be.scri.helpers.SuggestionHandler +import be.scri.helpers.clipboard.ClipboardMonitor import be.scri.helpers.data.AutocompletionDataManager import be.scri.helpers.english.ENInterfaceVariables.ALREADY_PLURAL_MSG import be.scri.helpers.ui.KeyboardUIManager import be.scri.models.ScribeState import be.scri.views.KeyboardView +import kotlinx.coroutines.launch import java.util.Locale private const val DATA_SIZE_2 = 2 @@ -115,6 +117,10 @@ abstract class GeneralKeyboardIME( internal val binding: InputMethodViewBinding get() = uiManager.binding + internal var hasNewClip: Boolean = false + internal var latestClipText: String? = null + private lateinit var clipboardMonitor: ClipboardMonitor + // MARK: State Variables internal var isSingularAndPlural: Boolean = false @@ -211,6 +217,14 @@ abstract class GeneralKeyboardIME( suggestionHandler = SuggestionHandler(this) autocompletionManager = dbManagers.autocompletionManager autocompletionHandler = AutocompletionHandler(this) + clipboardMonitor = + ClipboardMonitor(this) { text -> + latestClipText = text + hasNewClip = true + if (currentState == ScribeState.IDLE && this::uiManager.isInitialized) { + uiManager.showClipboardSuggestionChip(text) + } + } } override fun onDestroy() { @@ -339,6 +353,9 @@ abstract class GeneralKeyboardIME( restarting: Boolean, ) { super.onStartInputView(editorInfo, restarting) + if (this::clipboardMonitor.isInitialized) { + clipboardMonitor.startMonitoring() + } emojiAutoSuggestionEnabled = getIsEmojiSuggestionsEnabled(applicationContext, language) autoSuggestEmojis = null suggestionHandler.clearAllSuggestionsAndHideButtonUI() @@ -416,6 +433,9 @@ abstract class GeneralKeyboardIME( */ override fun onFinishInputView(finishingInput: Boolean) { super.onFinishInputView(finishingInput) + if (this::clipboardMonitor.isInitialized) { + clipboardMonitor.stopMonitoring() + } moveToIdleState() } @@ -510,6 +530,7 @@ abstract class GeneralKeyboardIME( KeyboardBase.KEYCODE_ENTER -> handleKeycodeEnter() KeyboardBase.KEYCODE_MODE_CHANGE -> handleModeChange(keyboardMode, keyboardView, this) + KeyboardBase.KEYCODE_CLIPBOARD -> openClipboardPanel() else -> { if (KeyboardBase.SCRIBE_VIEW_KEYS.contains(code)) { val keyLabel = keyboardView?.getKeyLabel(code) @@ -1950,8 +1971,84 @@ abstract class GeneralKeyboardIME( emojis: MutableList?, ) = uiManager.updateEmojiSuggestion(currentState, enabled, emojis) - /** - * Disables all auto-suggestions and resets the suggestion buttons to their default, inactive state. - */ fun disableAutoSuggest() = uiManager.disableAutoSuggest(language) + + override fun onClipboardSuggestionClicked() { + latestClipText?.let { text -> + currentInputConnection?.commitText(text, 1) + } + hideClipboardSuggestionChip() + } + + fun hideClipboardSuggestionChip() { + hasNewClip = false + latestClipText = null + if (this::uiManager.isInitialized) { + uiManager.hideClipboardSuggestionChip() + } + } + + private var clipboardAdapter: be.scri.helpers.clipboard.ClipboardAdapter? = null + private val clipboardRepository by lazy { + be.scri.helpers.clipboard + .ClipboardRepository(this) + } + + fun openClipboardPanel() { + if (!this::uiManager.isInitialized) return + uiManager.showClipboardPanel() + + val recyclerView = binding.clipboardItemsList + val emptyText = binding.clipboardEmptyText + + clipboardAdapter = + be.scri.helpers.clipboard.ClipboardAdapter( + items = emptyList(), + onItemClick = { item -> + currentInputConnection?.commitText(item.text, 1) + closeClipboardPanel() + }, + onItemDelete = { item -> + kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.Main).launch { + clipboardRepository.deleteItem(item.id) + refreshClipboardPanel() + } + }, + onItemPinToggle = { item -> + kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.Main).launch { + clipboardRepository.togglePin(item.id, item.isPinned) + refreshClipboardPanel() + } + }, + ) + recyclerView.adapter = clipboardAdapter + recyclerView.layoutManager = androidx.recyclerview.widget.GridLayoutManager(this, 2) + + binding.clipboardPanelClose.setOnClickListener { closeClipboardPanel() } + binding.clipboardClearAll.setOnClickListener { + kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.Main).launch { + clipboardRepository.clearAll() + refreshClipboardPanel() + } + } + + kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.Main).launch { + val items = clipboardRepository.getAllItems() + clipboardAdapter?.updateItems(items) + emptyText.visibility = if (items.isEmpty()) android.view.View.VISIBLE else android.view.View.GONE + recyclerView.visibility = if (items.isEmpty()) android.view.View.GONE else android.view.View.VISIBLE + } + } + + fun closeClipboardPanel() { + if (!this::uiManager.isInitialized) return + uiManager.hideClipboardPanel() + } + + private suspend fun refreshClipboardPanel() { + val items = clipboardRepository.getAllItems() + clipboardAdapter?.updateItems(items) + binding.clipboardEmptyText.visibility = if (items.isEmpty()) android.view.View.VISIBLE else android.view.View.GONE + binding.clipboardItemsList.visibility = if (items.isEmpty()) android.view.View.GONE else android.view.View.VISIBLE + } } diff --git a/app/src/main/java/be/scri/helpers/KeyboardBase.kt b/app/src/main/java/be/scri/helpers/KeyboardBase.kt index 4af94453..e3823bd9 100644 --- a/app/src/main/java/be/scri/helpers/KeyboardBase.kt +++ b/app/src/main/java/be/scri/helpers/KeyboardBase.kt @@ -88,6 +88,7 @@ class KeyboardBase { const val KEYCODE_CAPS_LOCK = -50 const val KEYCODE_LEFT_ARROW = -55 const val KEYCODE_RIGHT_ARROW = -56 + const val KEYCODE_CLIPBOARD = -60 const val SHIFT_OFF = 0 const val SHIFT_ON = 1 const val SHIFT_ON_PERMANENT = 2 @@ -658,6 +659,38 @@ class KeyboardBase { } } + if (currentKeyboardMode == keyboardLettersMode && mKeys != null) { + val spaceKey = mKeys!!.find { it?.code == 32 } + val commaKey = mKeys!!.find { it?.code == ','.code } + val row = mRows.lastOrNull() + if (spaceKey != null && commaKey != null && row != null) { + val clipWidth = (mDisplayWidth * 0.10).toInt() + + val clipKey = Key(row) + clipKey.code = KEYCODE_CLIPBOARD + clipKey.width = clipWidth + clipKey.height = spaceKey.height + clipKey.gap = spaceKey.gap + clipKey.icon = context.resources.getDrawable(R.drawable.ic_clipboard_vector, context.theme) + clipKey.icon?.setBounds(0, 0, clipKey.icon!!.intrinsicWidth, clipKey.icon!!.intrinsicHeight) + + spaceKey.width = spaceKey.width - clipWidth - clipKey.gap + + val commaIdxInList = mKeys!!.indexOf(commaKey) + val commaIdxInRow = row.mKeys.indexOf(commaKey) + + if (commaIdxInList != -1 && commaIdxInRow != -1) { + clipKey.x = commaKey.x + commaKey.width + commaKey.gap + clipKey.y = commaKey.y + + spaceKey.x = clipKey.x + clipKey.width + clipKey.gap + + mKeys!!.add(commaIdxInList + 1, clipKey) + row.mKeys.add(commaIdxInRow + 1, clipKey) + } + } + } + mHeight = y } diff --git a/app/src/main/java/be/scri/helpers/clipboard/ClipboardAdapter.kt b/app/src/main/java/be/scri/helpers/clipboard/ClipboardAdapter.kt new file mode 100644 index 00000000..711c9774 --- /dev/null +++ b/app/src/main/java/be/scri/helpers/clipboard/ClipboardAdapter.kt @@ -0,0 +1,72 @@ +package be.scri.helpers.clipboard + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.PopupMenu +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import be.scri.R + +class ClipboardAdapter( + private var items: List, + private val onItemClick: (ClipboardItem) -> Unit, + private val onItemDelete: (ClipboardItem) -> Unit, + private val onItemPinToggle: (ClipboardItem) -> Unit, +) : RecyclerView.Adapter() { + class ViewHolder( + view: View, + ) : RecyclerView.ViewHolder(view) { + val clipText: TextView = view.findViewById(R.id.clip_text) + val pinIcon: ImageView = view.findViewById(R.id.pin_icon) + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): ViewHolder { + val view = + LayoutInflater + .from(parent.context) + .inflate(R.layout.clipboard_item, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder( + holder: ViewHolder, + position: Int, + ) { + val item = items[position] + holder.clipText.text = item.text + holder.pinIcon.visibility = View.VISIBLE + + holder.itemView.setOnClickListener { + onItemClick(item) + } + + holder.itemView.setOnLongClickListener { view -> + val context = view.context + val popup = PopupMenu(context, view) + popup.menu.add(if (item.isPinned) "Unpin" else "Pin") + popup.menu.add("Delete") + + popup.setOnMenuItemClickListener { menuItem -> + when (menuItem.title) { + "Pin", "Unpin" -> onItemPinToggle(item) + "Delete" -> onItemDelete(item) + } + true + } + popup.show() + true + } + } + + override fun getItemCount(): Int = items.size + + fun updateItems(newItems: List) { + items = newItems + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/be/scri/helpers/clipboard/ClipboardDao.kt b/app/src/main/java/be/scri/helpers/clipboard/ClipboardDao.kt new file mode 100644 index 00000000..cc92d289 --- /dev/null +++ b/app/src/main/java/be/scri/helpers/clipboard/ClipboardDao.kt @@ -0,0 +1,45 @@ +package be.scri.helpers.clipboard + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query + +@Dao +interface ClipboardDao { + @Query("SELECT * FROM clipboard_items ORDER BY isPinned DESC, timestamp DESC") + suspend fun getAll(): List + + @Query("SELECT * FROM clipboard_items WHERE text = :text LIMIT 1") + suspend fun getByText(text: String): ClipboardItem? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(item: ClipboardItem): Long + + @Query("DELETE FROM clipboard_items WHERE id = :id") + suspend fun deleteById(id: Long) + + @Query("DELETE FROM clipboard_items") + suspend fun deleteAll() + + @Query("UPDATE clipboard_items SET isPinned = :pinned WHERE id = :id") + suspend fun updatePinned( + id: Long, + pinned: Boolean, + ) + + @Query("SELECT COUNT(*) FROM clipboard_items WHERE isPinned = 0") + suspend fun getUnpinnedCount(): Int + + @Query("SELECT * FROM clipboard_items WHERE isPinned = 0 ORDER BY timestamp ASC LIMIT 1") + suspend fun getOldestUnpinned(): ClipboardItem? + + @Query("DELETE FROM clipboard_items WHERE isPinned = 0 AND timestamp < :expiryTime") + suspend fun deleteExpired(expiryTime: Long) + + @Query("UPDATE clipboard_items SET timestamp = :timestamp WHERE id = :id") + suspend fun updateTimestamp( + id: Long, + timestamp: Long, + ) +} diff --git a/app/src/main/java/be/scri/helpers/clipboard/ClipboardDatabase.kt b/app/src/main/java/be/scri/helpers/clipboard/ClipboardDatabase.kt new file mode 100644 index 00000000..6ff3b42f --- /dev/null +++ b/app/src/main/java/be/scri/helpers/clipboard/ClipboardDatabase.kt @@ -0,0 +1,29 @@ +package be.scri.helpers.clipboard + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database(entities = [ClipboardItem::class], version = 1, exportSchema = false) +abstract class ClipboardDatabase : RoomDatabase() { + abstract fun clipboardDao(): ClipboardDao + + companion object { + @Volatile + private var instance: ClipboardDatabase? = null + + fun getDatabase(context: Context): ClipboardDatabase = + instance ?: synchronized(this) { + val newInstance = + Room + .databaseBuilder( + context.applicationContext, + ClipboardDatabase::class.java, + "scribe_clipboard_db", + ).build() + instance = newInstance + newInstance + } + } +} diff --git a/app/src/main/java/be/scri/helpers/clipboard/ClipboardItem.kt b/app/src/main/java/be/scri/helpers/clipboard/ClipboardItem.kt new file mode 100644 index 00000000..e5cae10a --- /dev/null +++ b/app/src/main/java/be/scri/helpers/clipboard/ClipboardItem.kt @@ -0,0 +1,12 @@ +package be.scri.helpers.clipboard + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "clipboard_items") +data class ClipboardItem( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + val text: String, + val timestamp: Long = System.currentTimeMillis(), + val isPinned: Boolean = false, +) diff --git a/app/src/main/java/be/scri/helpers/clipboard/ClipboardMonitor.kt b/app/src/main/java/be/scri/helpers/clipboard/ClipboardMonitor.kt new file mode 100644 index 00000000..e8718a9d --- /dev/null +++ b/app/src/main/java/be/scri/helpers/clipboard/ClipboardMonitor.kt @@ -0,0 +1,71 @@ +package be.scri.helpers.clipboard + +import android.content.ClipDescription +import android.content.ClipboardManager +import android.content.Context +import android.os.Build +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class ClipboardMonitor( + private val context: Context, + private val onNewClip: (String) -> Unit, +) { + private val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + private val repository = ClipboardRepository(context) + private val scope = CoroutineScope(Dispatchers.Main) + + private var lastProcessedText: String? = null + private var lastProcessedTime: Long = 0L + private val debounceMs = 1000L + + private val listener = + ClipboardManager.OnPrimaryClipChangedListener { + processCurrentClip() + } + + fun startMonitoring() { + try { + clipboardManager.removePrimaryClipChangedListener(listener) + clipboardManager.addPrimaryClipChangedListener(listener) + } catch (e: SecurityException) { + Log.e("ClipboardMonitor", "Failed to add primary clip changed listener", e) + } + } + + fun stopMonitoring() { + try { + clipboardManager.removePrimaryClipChangedListener(listener) + } catch (e: SecurityException) { + Log.e("ClipboardMonitor", "Failed to remove primary clip changed listener", e) + } + } + + private fun processCurrentClip() { + val clip = clipboardManager.primaryClip ?: return + if (clip.itemCount == 0) return + + val description = clipboardManager.primaryClipDescription + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (description?.extras?.getBoolean(ClipDescription.EXTRA_IS_SENSITIVE) == true) { + return + } + } + + val text = clip.getItemAt(0).text?.toString() ?: return + if (text.isBlank()) return + + val now = System.currentTimeMillis() + if (text == lastProcessedText && now - lastProcessedTime < debounceMs) return + lastProcessedText = text + lastProcessedTime = now + + scope.launch { + repository.insertItem(text) + onNewClip(text) + } + } +} diff --git a/app/src/main/java/be/scri/helpers/clipboard/ClipboardRepository.kt b/app/src/main/java/be/scri/helpers/clipboard/ClipboardRepository.kt new file mode 100644 index 00000000..0aa2d851 --- /dev/null +++ b/app/src/main/java/be/scri/helpers/clipboard/ClipboardRepository.kt @@ -0,0 +1,66 @@ +package be.scri.helpers.clipboard + +import android.content.Context +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class ClipboardRepository( + context: Context, +) { + private val clipboardDao = ClipboardDatabase.getDatabase(context).clipboardDao() + + companion object { + private const val MAX_ITEMS = 25 + } + + private suspend fun pruneExpiredItems() { + val oneHourAgo = System.currentTimeMillis() - 3600000L + clipboardDao.deleteExpired(oneHourAgo) + } + + suspend fun getAllItems(): List = + withContext(Dispatchers.IO) { + pruneExpiredItems() + clipboardDao.getAll() + } + + suspend fun insertItem(text: String): Long = + withContext(Dispatchers.IO) { + pruneExpiredItems() + if (text.isBlank()) return@withContext -1L + + val existing = clipboardDao.getByText(text) + if (existing != null) { + clipboardDao.updateTimestamp(existing.id, System.currentTimeMillis()) + return@withContext existing.id + } + + val allItems = clipboardDao.getAll() + if (allItems.size >= MAX_ITEMS) { + val oldestUnpinned = clipboardDao.getOldestUnpinned() + if (oldestUnpinned != null) { + clipboardDao.deleteById(oldestUnpinned.id) + } + } + + val newItem = ClipboardItem(text = text) + clipboardDao.insert(newItem) + } + + suspend fun deleteItem(id: Long) = + withContext(Dispatchers.IO) { + clipboardDao.deleteById(id) + } + + suspend fun togglePin( + id: Long, + isPinned: Boolean, + ) = withContext(Dispatchers.IO) { + clipboardDao.updatePinned(id, !isPinned) + } + + suspend fun clearAll() = + withContext(Dispatchers.IO) { + clipboardDao.deleteAll() + } +} diff --git a/app/src/main/java/be/scri/views/KeyboardView.kt b/app/src/main/java/be/scri/views/KeyboardView.kt index 0e959a38..dbc77e35 100644 --- a/app/src/main/java/be/scri/views/KeyboardView.kt +++ b/app/src/main/java/be/scri/views/KeyboardView.kt @@ -1140,8 +1140,15 @@ class KeyboardView } key.icon = resources.getDrawable(drawableId) key.icon!!.applyColorFilter(mTextColor) - } else if (code == KEYCODE_DELETE || code == KEYCODE_SHIFT || code == KEYCODE_TAB) { - key.icon!!.applyColorFilter(mTextColor) + } else { + val isIconOnlyKey = + code == KEYCODE_DELETE || + code == KEYCODE_SHIFT || + code == KEYCODE_TAB || + code == KeyboardBase.KEYCODE_CLIPBOARD + if (isIconOnlyKey) { + key.icon!!.applyColorFilter(mTextColor) + } } // Controls where icons are located on their keys. diff --git a/app/src/main/res/drawable/clipboard_item_bg.xml b/app/src/main/res/drawable/clipboard_item_bg.xml new file mode 100644 index 00000000..bf0a5428 --- /dev/null +++ b/app/src/main/res/drawable/clipboard_item_bg.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_clipboard_vector.xml b/app/src/main/res/drawable/ic_clipboard_vector.xml index bf8c4063..0ade0536 100644 --- a/app/src/main/res/drawable/ic_clipboard_vector.xml +++ b/app/src/main/res/drawable/ic_clipboard_vector.xml @@ -1,3 +1,4 @@ + + android:pathData="M19,2h-4.18C14.4,0.84 13.3,0 12,0S9.6,0.84 9.18,2H5C3.9,2 3,2.9 3,4v16c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V4C21,2.9 20.1,2 19,2zM12,2c0.55,0 1,0.45 1,1s-0.45,1 -1,1s-1,-0.45 -1,-1S11.45,2 12,2zM19,20H5V4h2v3h10V4h2V20z"/> diff --git a/app/src/main/res/drawable/ic_copy_vector.xml b/app/src/main/res/drawable/ic_copy_vector.xml new file mode 100644 index 00000000..e2c2241d --- /dev/null +++ b/app/src/main/res/drawable/ic_copy_vector.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_delete_vector.xml b/app/src/main/res/drawable/ic_delete_vector.xml index 7fc46c5f..c224bd5e 100644 --- a/app/src/main/res/drawable/ic_delete_vector.xml +++ b/app/src/main/res/drawable/ic_delete_vector.xml @@ -1,4 +1,10 @@ - - - + + + diff --git a/app/src/main/res/layout/clipboard_item.xml b/app/src/main/res/layout/clipboard_item.xml new file mode 100644 index 00000000..8828a64b --- /dev/null +++ b/app/src/main/res/layout/clipboard_item.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/input_method_view.xml b/app/src/main/res/layout/input_method_view.xml index f79cf813..77db934f 100644 --- a/app/src/main/res/layout/input_method_view.xml +++ b/app/src/main/res/layout/input_method_view.xml @@ -155,6 +155,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/separator_1" app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -406,6 +407,25 @@ app:layout_constraintStart_toEndOf="@+id/emoji_space_tablet_2" app:layout_constraintTop_toTopOf="@+id/plural_btn" /> +