diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/AppPreference.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/AppPreference.kt index 6b0bc6e2..8e7cdd51 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/AppPreference.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/AppPreference.kt @@ -34,6 +34,8 @@ object AppPreference { const val DEFAULT_CUSTOM_THEME_CANDIDATE_ITEM_BG_COLOR = 0x00000000 const val DEFAULT_CUSTOM_THEME_CANDIDATE_ITEM_PRESSED_BG_COLOR = 0xFFF0F0F3.toInt() + private const val MIN_CANDIDATE_VISIBLE_HEIGHT_DP = 30 + private const val MAX_CANDIDATE_VISIBLE_HEIGHT_DP = 300 private lateinit var preferences: SharedPreferences private val gson = Gson() @@ -208,7 +210,7 @@ object AppPreference { Pair("qwerty_keyboard_position_landscape_preference", true) private val CANDIDATE_VIEW_HEIGHT_DP_LANDSCAPE = - Pair("candidate_view_height_dp_landscape_preference", 110) + Pair("candidate_view_height_dp_landscape_preference", 60) private val CANDIDATE_VIEW_EMPTY_HEIGHT_DP_LANDSCAPE = Pair("candidate_view_empty_height_dp_landscape_preference", 110) @@ -398,6 +400,32 @@ object AppPreference { private val CANDIDATE_VIEW_HEIGHT_DP = Pair("candidate_view_height_dp_preference", 110) private val CANDIDATE_VIEW_EMPTY_HEIGHT_DP = Pair("candidate_view_empty_height_dp_preference", 110) + private val CANDIDATE_VIEW_HEIGHT_PORTRAIT_COLUMN_1_DP = + Pair("candidate_view_height_portrait_column_1_dp_preference", 110) + private val CANDIDATE_VIEW_HEIGHT_PORTRAIT_COLUMN_2_DP = + Pair("candidate_view_height_portrait_column_2_dp_preference", 120) + private val CANDIDATE_VIEW_HEIGHT_PORTRAIT_COLUMN_3_DP = + Pair("candidate_view_height_portrait_column_3_dp_preference", 160) + private val CANDIDATE_VIEW_HEIGHT_LANDSCAPE_COLUMN_1_DP = + Pair("candidate_view_height_landscape_column_1_dp_preference", 60) + private val CANDIDATE_VIEW_HEIGHT_LANDSCAPE_COLUMN_2_DP = + Pair("candidate_view_height_landscape_column_2_dp_preference", 90) + private val CANDIDATE_VIEW_HEIGHT_LANDSCAPE_COLUMN_3_DP = + Pair("candidate_view_height_landscape_column_3_dp_preference", 120) + private val CANDIDATE_DEFAULT_HEIGHT_PORTRAIT_COLUMN_1_DP = + Pair("candidate_default_height_portrait_column_1_dp_preference", 110) + private val CANDIDATE_DEFAULT_HEIGHT_PORTRAIT_COLUMN_2_DP = + Pair("candidate_default_height_portrait_column_2_dp_preference", 120) + private val CANDIDATE_DEFAULT_HEIGHT_PORTRAIT_COLUMN_3_DP = + Pair("candidate_default_height_portrait_column_3_dp_preference", 160) + private val CANDIDATE_DEFAULT_HEIGHT_LANDSCAPE_COLUMN_1_DP = + Pair("candidate_default_height_landscape_column_1_dp_preference", 60) + private val CANDIDATE_DEFAULT_HEIGHT_LANDSCAPE_COLUMN_2_DP = + Pair("candidate_default_height_landscape_column_2_dp_preference", 90) + private val CANDIDATE_DEFAULT_HEIGHT_LANDSCAPE_COLUMN_3_DP = + Pair("candidate_default_height_landscape_column_3_dp_preference", 120) + private val CANDIDATE_HEIGHT_PER_COLUMN_MIGRATED = + Pair("candidate_height_per_column_migrated_preference", false) private val CLIP_BOARD_PREVIEW_PREFERENCE = Pair("clipboard_preview_enable_preference", true) @@ -668,6 +696,59 @@ object AppPreference { }.getOrDefault(defaultValue) } + private fun normalizeCandidateColumn(column: String): String = + if (column in setOf("1", "2", "3")) column else "1" + + private fun candidateHeightPreferenceFor( + isLandscape: Boolean, + column: String + ): Pair { + return when (normalizeCandidateColumn(column)) { + "2" -> if (isLandscape) { + CANDIDATE_VIEW_HEIGHT_LANDSCAPE_COLUMN_2_DP + } else { + CANDIDATE_VIEW_HEIGHT_PORTRAIT_COLUMN_2_DP + } + + "3" -> if (isLandscape) { + CANDIDATE_VIEW_HEIGHT_LANDSCAPE_COLUMN_3_DP + } else { + CANDIDATE_VIEW_HEIGHT_PORTRAIT_COLUMN_3_DP + } + + else -> if (isLandscape) { + CANDIDATE_VIEW_HEIGHT_LANDSCAPE_COLUMN_1_DP + } else { + CANDIDATE_VIEW_HEIGHT_PORTRAIT_COLUMN_1_DP + } + } + } + + private fun candidateDefaultHeightPreferenceFor( + isLandscape: Boolean, + column: String + ): Pair { + return when (normalizeCandidateColumn(column)) { + "2" -> if (isLandscape) { + CANDIDATE_DEFAULT_HEIGHT_LANDSCAPE_COLUMN_2_DP + } else { + CANDIDATE_DEFAULT_HEIGHT_PORTRAIT_COLUMN_2_DP + } + + "3" -> if (isLandscape) { + CANDIDATE_DEFAULT_HEIGHT_LANDSCAPE_COLUMN_3_DP + } else { + CANDIDATE_DEFAULT_HEIGHT_PORTRAIT_COLUMN_3_DP + } + + else -> if (isLandscape) { + CANDIDATE_DEFAULT_HEIGHT_LANDSCAPE_COLUMN_1_DP + } else { + CANDIDATE_DEFAULT_HEIGHT_PORTRAIT_COLUMN_1_DP + } + } + } + var clipboard_history_enable: Boolean? get() = preferences.getBoolean( CLIPBOARD_HISTORY_ENABLE.first, CLIPBOARD_HISTORY_ENABLE.second @@ -1639,6 +1720,199 @@ object AppPreference { it.putString(CANDIDATE_COLUMN_LANDSCAPE_PREFERENCE.first, value) } + fun migrateCandidateHeightPerColumnPreferencesIfNeeded() { + if (preferences.getBoolean( + CANDIDATE_HEIGHT_PER_COLUMN_MIGRATED.first, + CANDIDATE_HEIGHT_PER_COLUMN_MIGRATED.second + ) + ) { + return + } + + val portraitColumn = normalizeCandidateColumn(candidate_column_preference) + val landscapeColumn = normalizeCandidateColumn(candidate_column_landscape_preference) + val portraitHeight = (candidate_view_height_dp + ?: CANDIDATE_VIEW_HEIGHT_DP.second).coerceIn(MIN_CANDIDATE_VISIBLE_HEIGHT_DP, MAX_CANDIDATE_VISIBLE_HEIGHT_DP) + val landscapeHeight = (candidate_view_height_dp_landscape + ?: CANDIDATE_VIEW_HEIGHT_DP_LANDSCAPE.second).coerceIn(MIN_CANDIDATE_VISIBLE_HEIGHT_DP, MAX_CANDIDATE_VISIBLE_HEIGHT_DP) + + preferences.edit { editor -> + candidateHeightPreferenceFor(isLandscape = false, column = "1").let { editor.putInt(it.first, it.second) } + candidateHeightPreferenceFor(isLandscape = false, column = "2").let { editor.putInt(it.first, it.second) } + candidateHeightPreferenceFor(isLandscape = false, column = "3").let { editor.putInt(it.first, it.second) } + candidateHeightPreferenceFor(isLandscape = true, column = "1").let { editor.putInt(it.first, it.second) } + candidateHeightPreferenceFor(isLandscape = true, column = "2").let { editor.putInt(it.first, it.second) } + candidateHeightPreferenceFor(isLandscape = true, column = "3").let { editor.putInt(it.first, it.second) } + editor.putInt(candidateHeightPreferenceFor(false, portraitColumn).first, portraitHeight) + editor.putInt(candidateHeightPreferenceFor(true, landscapeColumn).first, landscapeHeight) + editor.putBoolean(CANDIDATE_HEIGHT_PER_COLUMN_MIGRATED.first, true) + } + } + + fun getCandidateVisibleHeightDp( + isLandscape: Boolean, + column: String + ): Int { + migrateCandidateHeightPerColumnPreferencesIfNeeded() + val preference = candidateHeightPreferenceFor(isLandscape, normalizeCandidateColumn(column)) + return readIntPreference(preference.first, preference.second) + .coerceIn(MIN_CANDIDATE_VISIBLE_HEIGHT_DP, MAX_CANDIDATE_VISIBLE_HEIGHT_DP) + } + + fun setCandidateVisibleHeightDp( + isLandscape: Boolean, + column: String, + heightDp: Int + ) { + migrateCandidateHeightPerColumnPreferencesIfNeeded() + val normalizedColumn = normalizeCandidateColumn(column) + val clampedHeight = heightDp.coerceIn( + MIN_CANDIDATE_VISIBLE_HEIGHT_DP, + MAX_CANDIDATE_VISIBLE_HEIGHT_DP + ) + val preference = candidateHeightPreferenceFor(isLandscape, normalizedColumn) + preferences.edit { editor -> + editor.putInt(preference.first, clampedHeight) + if (normalizeCandidateColumn(getCandidateColumn(isLandscape)) == normalizedColumn) { + editor.putInt( + if (isLandscape) { + CANDIDATE_VIEW_HEIGHT_DP_LANDSCAPE.first + } else { + CANDIDATE_VIEW_HEIGHT_DP.first + }, + clampedHeight + ) + } + } + } + + fun getCandidateDefaultVisibleHeightDp( + isLandscape: Boolean, + column: String + ): Int { + val preference = candidateDefaultHeightPreferenceFor( + isLandscape, + normalizeCandidateColumn(column) + ) + return readIntPreference(preference.first, preference.second) + .coerceIn(MIN_CANDIDATE_VISIBLE_HEIGHT_DP, MAX_CANDIDATE_VISIBLE_HEIGHT_DP) + } + + fun setCandidateDefaultVisibleHeightDp( + isLandscape: Boolean, + column: String, + heightDp: Int + ) { + val preference = candidateDefaultHeightPreferenceFor( + isLandscape, + normalizeCandidateColumn(column) + ) + preferences.edit { editor -> + editor.putInt( + preference.first, + heightDp.coerceIn( + MIN_CANDIDATE_VISIBLE_HEIGHT_DP, + MAX_CANDIDATE_VISIBLE_HEIGHT_DP + ) + ) + } + } + + fun resetCandidateVisibleHeightsToUserDefaults(isLandscape: Boolean) { + migrateCandidateHeightPerColumnPreferencesIfNeeded() + val activeColumn = getCandidateColumn(isLandscape) + preferences.edit { editor -> + listOf("1", "2", "3").forEach { column -> + val heightDp = getCandidateDefaultVisibleHeightDp(isLandscape, column) + editor.putInt(candidateHeightPreferenceFor(isLandscape, column).first, heightDp) + if (column == activeColumn) { + editor.putInt( + if (isLandscape) { + CANDIDATE_VIEW_HEIGHT_DP_LANDSCAPE.first + } else { + CANDIDATE_VIEW_HEIGHT_DP.first + }, + heightDp + ) + } + } + } + } + + fun resetCandidateDefaultVisibleHeightsToFactoryDefaults(isLandscape: Boolean) { + preferences.edit { editor -> + listOf("1", "2", "3").forEach { column -> + val preference = candidateDefaultHeightPreferenceFor(isLandscape, column) + editor.putInt(preference.first, preference.second) + } + } + } + + fun copyCandidateVisibleHeightsToUserDefaults(isLandscape: Boolean) { + migrateCandidateHeightPerColumnPreferencesIfNeeded() + preferences.edit { editor -> + listOf("1", "2", "3").forEach { column -> + val heightDp = getCandidateVisibleHeightDp(isLandscape, column) + editor.putInt( + candidateDefaultHeightPreferenceFor(isLandscape, column).first, + heightDp + ) + } + } + } + + fun getCandidateColumn(isLandscape: Boolean): String = + normalizeCandidateColumn( + if (isLandscape) { + candidate_column_landscape_preference + } else { + candidate_column_preference + } + ) + + fun setCandidateColumnAndSyncHeight( + isLandscape: Boolean, + column: String + ) { + migrateCandidateHeightPerColumnPreferencesIfNeeded() + val normalizedColumn = normalizeCandidateColumn(column) + val heightDp = getCandidateVisibleHeightDp(isLandscape, normalizedColumn) + preferences.edit { editor -> + editor.putString( + if (isLandscape) { + CANDIDATE_COLUMN_LANDSCAPE_PREFERENCE.first + } else { + CANDIDATE_COLUMN_PREFERENCE.first + }, + normalizedColumn + ) + editor.putInt( + if (isLandscape) { + CANDIDATE_VIEW_HEIGHT_DP_LANDSCAPE.first + } else { + CANDIDATE_VIEW_HEIGHT_DP.first + }, + heightDp + ) + } + } + + fun syncActiveCandidateVisibleHeightToImePreference(isLandscape: Boolean) { + migrateCandidateHeightPerColumnPreferencesIfNeeded() + val column = getCandidateColumn(isLandscape) + val heightDp = getCandidateVisibleHeightDp(isLandscape, column) + preferences.edit { editor -> + editor.putInt( + if (isLandscape) { + CANDIDATE_VIEW_HEIGHT_DP_LANDSCAPE.first + } else { + CANDIDATE_VIEW_HEIGHT_DP.first + }, + heightDp + ) + } + } + var candidate_tab_preference: Boolean get() = preferences.getBoolean( CANDIDATE_TAB_PREFERENCE.first, CANDIDATE_TAB_PREFERENCE.second diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_landscape_setting/CandidateHeightLandscapeSettingFragment.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_landscape_setting/CandidateHeightLandscapeSettingFragment.kt index 9cb9c568..3cd86891 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_landscape_setting/CandidateHeightLandscapeSettingFragment.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_landscape_setting/CandidateHeightLandscapeSettingFragment.kt @@ -1,29 +1,52 @@ package com.kazumaproject.markdownhelperkeyboard.setting_activity.ui.candidate_view_height_landscape_setting import android.annotation.SuppressLint +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.drawable.GradientDrawable import android.os.Bundle +import android.view.Gravity import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.LinearLayout import android.widget.SeekBar -import androidx.core.view.MenuHost -import androidx.core.view.MenuProvider +import androidx.annotation.AttrRes +import androidx.appcompat.R as AppCompatR +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.AppCompatImageButton +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.view.isInvisible +import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.R as MaterialR +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import com.google.android.material.textview.MaterialTextView import com.kazumaproject.markdownhelperkeyboard.R -import com.kazumaproject.markdownhelperkeyboard.converter.candidate.Candidate import com.kazumaproject.markdownhelperkeyboard.databinding.FragmentCandidateHeightLandscapeSettingBinding -import com.kazumaproject.markdownhelperkeyboard.ime_service.adapters.GridSpacingItemDecoration +import com.kazumaproject.markdownhelperkeyboard.ime_service.CandidateStripPresentationPolicy +import com.kazumaproject.markdownhelperkeyboard.ime_service.CandidateStripPresentationState +import com.kazumaproject.markdownhelperkeyboard.ime_service.state.CandidateTab +import com.kazumaproject.markdownhelperkeyboard.repository.KeyboardRepository import com.kazumaproject.markdownhelperkeyboard.setting_activity.AppPreference +import com.kazumaproject.markdownhelperkeyboard.setting_activity.ui.candidate_view_height_setting.CandidateKeyboardPreviewViews +import com.kazumaproject.markdownhelperkeyboard.setting_activity.ui.candidate_view_height_setting.CandidateHeightPreviewGridSpacingDecoration import com.kazumaproject.markdownhelperkeyboard.setting_activity.ui.candidate_view_height_setting.SuggestionAdapter2 +import com.kazumaproject.markdownhelperkeyboard.setting_activity.ui.candidate_view_height_setting.candidateKeyboardPreviewHeightPx +import com.kazumaproject.markdownhelperkeyboard.setting_activity.ui.candidate_view_height_setting.clearItemDecorations +import com.kazumaproject.markdownhelperkeyboard.setting_activity.ui.candidate_view_height_setting.createCandidateHeightPreviewCandidates +import com.kazumaproject.markdownhelperkeyboard.setting_activity.ui.candidate_view_height_setting.renderCandidateKeyboardPreview +import com.kazumaproject.markdownhelperkeyboard.short_cut.ShortcutType +import com.kazumaproject.markdownhelperkeyboard.sumire_special_key.SumireSpecialKeyRepository import dagger.hilt.android.AndroidEntryPoint import timber.log.Timber +import java.util.Locale import javax.inject.Inject import kotlin.math.roundToInt @@ -33,41 +56,48 @@ class CandidateHeightLandscapeSettingFragment : Fragment() { @Inject lateinit var appPreference: AppPreference + @Inject + lateinit var keyboardRepository: KeyboardRepository + + @Inject + lateinit var sumireSpecialKeyRepository: SumireSpecialKeyRepository + private lateinit var suggestionAdapter: SuggestionAdapter2 - private lateinit var candidateList: List private var _binding: FragmentCandidateHeightLandscapeSettingBinding? = null private val binding get() = _binding!! - private var isCandidateListVisible = false + private var isCandidateListVisible = true private var isSyncingHeightControls = false + private var isSyncingLetterSizeControls = false + private var isSyncingColumnControls = false + private var isSyncingDefaultHeightControls = false + private var previousBottomNavigationVisibility: Int? = null + private var previousNavHostBottomToTop = ConstraintLayout.LayoutParams.UNSET + private var previousNavHostBottomToBottom = ConstraintLayout.LayoutParams.UNSET + private var previousNavHostBottomMargin = 0 + private var hasPreviousNavHostBottomConstraint = false private val minHeightDp = 30 private val maxHeightDp = 300 - private val defaultHeightDp = 110 + private val minCandidateTextSize = 10f + private val maxCandidateTextSize = 40f + private val defaultCandidateTextSize = 14.0f + + private val previewCandidates = createCandidateHeightPreviewCandidates() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) suggestionAdapter = SuggestionAdapter2() - candidateList = (1..16).map { index -> - Candidate( - string = "候補 $index", - type = (index % 4).toByte(), - length = "候補 $index".length.toUByte(), - score = 100 - index, - leftId = (index * 10).toShort(), - rightId = (index * 10 + 1).toShort() - ) - } } override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, + inflater: LayoutInflater, + container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = FragmentCandidateHeightLandscapeSettingBinding.inflate(inflater, container, false) - setupMenu() return binding.root } @@ -75,89 +105,268 @@ class CandidateHeightLandscapeSettingFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + hideBottomNavigationForPreview() + appPreference.migrateCandidateHeightPerColumnPreferencesIfNeeded() + appPreference.syncActiveCandidateVisibleHeightToImePreference(isLandscape = true) + + setupMenu() + setupAdapter() + setupInspectorBottomSheet() + setupInspectorTabs() + setupColumnControls() setupResizeHandle() + setupHeightSeekBar() + setupHeightEditText() + setupCandidateLetterSizeSeekBar() + setupCandidateLetterSizeEditText() + setupDefaultHeightControls() + setupKeyboardPreview() setSuggestionView() - suggestionAdapter.apply { - setUndoEnabled(false) - setPasteEnabled(false) - onListUpdated = { - applyCurrentDimensions() - } - } - binding.toggleCandidateListButton.setOnClickListener { isCandidateListVisible = !isCandidateListVisible updateCandidateListAndHeight() } - setupHeightSeekBar() - setupHeightEditText() - + applyCandidateTextSize(appPreference.candidate_letter_size ?: defaultCandidateTextSize, persist = false) updateCandidateListAndHeight() applyHeightDp(selectedHeightDp(), persist = false) + syncDefaultHeightControls() + updateInspectorSummary() } - override fun onDestroyView() { super.onDestroyView() - isCandidateListVisible = false + restoreBottomNavigationVisibility() + (activity as? AppCompatActivity)?.supportActionBar?.show() + binding.candidateHeightSettingRecyclerview.adapter = null + suggestionAdapter.release() _binding = null } + private fun hideBottomNavigationForPreview() { + val bottomNavigation = activity?.findViewById(R.id.nav_view) ?: return + if (previousBottomNavigationVisibility == null) { + previousBottomNavigationVisibility = bottomNavigation.visibility + } + bottomNavigation.visibility = View.GONE + extendNavHostToParentBottom() + } + + private fun restoreBottomNavigationVisibility() { + val visibility = previousBottomNavigationVisibility ?: return + restoreNavHostBottomConstraint() + activity?.findViewById(R.id.nav_view)?.visibility = visibility + previousBottomNavigationVisibility = null + } + + private fun extendNavHostToParentBottom() { + val container = activity?.findViewById(R.id.container) ?: return + val navHost = activity?.findViewById(R.id.nav_host_fragment_activity_main) ?: return + val layoutParams = navHost.layoutParams as? ConstraintLayout.LayoutParams ?: return + if (!hasPreviousNavHostBottomConstraint) { + previousNavHostBottomToTop = layoutParams.bottomToTop + previousNavHostBottomToBottom = layoutParams.bottomToBottom + previousNavHostBottomMargin = layoutParams.bottomMargin + hasPreviousNavHostBottomConstraint = true + } + ConstraintSet().apply { + clone(container) + clear(R.id.nav_host_fragment_activity_main, ConstraintSet.BOTTOM) + connect( + R.id.nav_host_fragment_activity_main, + ConstraintSet.BOTTOM, + ConstraintSet.PARENT_ID, + ConstraintSet.BOTTOM + ) + setMargin(R.id.nav_host_fragment_activity_main, ConstraintSet.BOTTOM, 0) + applyTo(container) + } + } + + private fun restoreNavHostBottomConstraint() { + if (!hasPreviousNavHostBottomConstraint) return + val container = activity?.findViewById(R.id.container) ?: return + ConstraintSet().apply { + clone(container) + clear(R.id.nav_host_fragment_activity_main, ConstraintSet.BOTTOM) + when { + previousNavHostBottomToTop != ConstraintLayout.LayoutParams.UNSET -> { + connect( + R.id.nav_host_fragment_activity_main, + ConstraintSet.BOTTOM, + previousNavHostBottomToTop, + ConstraintSet.TOP + ) + } + + previousNavHostBottomToBottom != ConstraintLayout.LayoutParams.UNSET -> { + connect( + R.id.nav_host_fragment_activity_main, + ConstraintSet.BOTTOM, + previousNavHostBottomToBottom, + ConstraintSet.BOTTOM + ) + } + } + setMargin( + R.id.nav_host_fragment_activity_main, + ConstraintSet.BOTTOM, + previousNavHostBottomMargin + ) + applyTo(container) + } + hasPreviousNavHostBottomConstraint = false + } + private fun setupMenu() { - val menuHost: MenuHost = requireActivity() - menuHost.addMenuProvider(object : MenuProvider { - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - menuInflater.inflate(R.menu.fragment_reset_menu, menu) + (activity as? AppCompatActivity)?.supportActionBar?.hide() + binding.toolbar.setNavigationIcon(AppCompatR.drawable.abc_ic_ab_back_material) + binding.toolbar.setNavigationOnClickListener { + parentFragmentManager.popBackStack() + } + binding.toolbar.inflateMenu(R.menu.fragment_reset_menu) + binding.toolbar.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.action_candidate_default_height -> { + openDefaultHeightInspector() + true + } + + R.id.action_reset -> { + resetSettings() + true + } + + else -> false } + } + } - override fun onMenuItemSelected(menuItem: MenuItem): Boolean { - return when (menuItem.itemId) { - R.id.action_reset -> { - resetSettings() - true - } + private fun setupInspectorBottomSheet() { + binding.showInspectorButton.setOnClickListener { + showInspectorPanel() + } + binding.closeInspectorButton.setOnClickListener { + hideInspectorPanel() + } + showInspectorPanel() + } - android.R.id.home -> { - parentFragmentManager.popBackStack() - true - } + private fun setupInspectorTabs() { + binding.candidateInspectorTabGroup.check(R.id.candidate_tab_height_button) + binding.candidateInspectorTabGroup.addOnButtonCheckedListener { _, checkedId, isChecked -> + if (!isChecked) return@addOnButtonCheckedListener + showInspectorTab(checkedId) + } + showInspectorTab(R.id.candidate_tab_height_button) + } - else -> false - } + private fun openDefaultHeightInspector() { + binding.candidateInspectorTabGroup.check(R.id.candidate_tab_default_button) + syncDefaultHeightControls() + showInspectorPanel() + } + + private fun showInspectorPanel() { + binding.inspectorBottomSheet.isVisible = true + binding.showInspectorButton.isVisible = false + } + + private fun hideInspectorPanel() { + binding.inspectorBottomSheet.isVisible = false + binding.showInspectorButton.isVisible = true + } + + private fun showInspectorTab(checkedId: Int) { + binding.heightControlsContainer.isVisible = checkedId == R.id.candidate_tab_height_button + binding.textControlsContainer.isVisible = checkedId == R.id.candidate_tab_text_button + binding.defaultControlsContainer.isVisible = + checkedId == R.id.candidate_tab_default_button + if (checkedId == R.id.candidate_tab_default_button) { + syncDefaultHeightControls() + } + } + + private fun setupAdapter() { + suggestionAdapter.apply { + setUndoEnabled(false) + setPasteEnabled(false) + onListUpdated = { + applyCurrentDimensions() } - }, viewLifecycleOwner, Lifecycle.State.RESUMED) + } + applyCandidateAdapterPresentation() + binding.candidateHeightSettingRecyclerview.apply { + itemAnimator = null + isFocusable = false + } + binding.candidateHeightSettingRecyclerview.adapter = suggestionAdapter } - private fun resetSettings() { - // Reset landscape-specific preferences - if (!isCandidateListVisible) { - appPreference.candidate_view_height_dp_landscape = defaultHeightDp + private fun applyCandidateAdapterPresentation() { + suggestionAdapter.setShowCandidateYomiForLiveConversion( + (appPreference.live_conversion_preference ?: false) && + (appPreference.live_conversion_candidate_yomi_preference ?: false) + ) + if (appPreference.theme_mode == "custom") { + suggestionAdapter.setCandidateTextColor(appPreference.custom_theme_candidate_text_color) + suggestionAdapter.setCandidateItemColors( + backgroundColor = appPreference.custom_theme_candidate_item_bg_color, + pressedColor = appPreference.custom_theme_candidate_item_pressed_bg_color + ) + suggestionAdapter.setCandidateEmptyPopupColors( + backgroundColor = appPreference.custom_theme_candidate_empty_popup_bg_color, + textColor = appPreference.custom_theme_candidate_empty_popup_text_color + ) } else { - when (appPreference.candidate_column_preference) { - "1" -> appPreference.candidate_view_height_dp_landscape = defaultHeightDp - "2" -> appPreference.candidate_view_height_dp_landscape = 165 - "3" -> appPreference.candidate_view_height_dp_landscape = 230 - } + suggestionAdapter.clearCandidateEmptyPopupColors() + } + } + + private fun setupColumnControls() { + syncColumnControls() + binding.candidateColumnToggleGroup.addOnButtonCheckedListener { _, checkedId, isChecked -> + if (!isChecked || isSyncingColumnControls) return@addOnButtonCheckedListener + val column = columnForButtonId(checkedId) ?: return@addOnButtonCheckedListener + appPreference.setCandidateColumnAndSyncHeight(isLandscape = true, column = column) + setSuggestionView() + syncColumnControls() + applyHeightDp(selectedHeightDp(), persist = false) + updateInspectorSummary() } - appPreference.candidate_view_empty_height_dp_landscape = defaultHeightDp + } + + private fun resetSettings() { + appPreference.resetCandidateVisibleHeightsToUserDefaults(isLandscape = true) + appPreference.syncActiveCandidateVisibleHeightToImePreference(isLandscape = true) + appPreference.candidate_letter_size = defaultCandidateTextSize + applyCandidateTextSize(defaultCandidateTextSize, persist = false) + syncColumnControls() + updateCandidateListAndHeight() applyHeightDp(selectedHeightDp(), persist = false) + updateInspectorSummary() } private fun updateCandidateListAndHeight() { if (isCandidateListVisible) { - suggestionAdapter.suggestions = candidateList - binding.toggleCandidateListButton.text = "入力時" + suggestionAdapter.suggestions = previewCandidates + binding.toggleCandidateListButton.text = getString(R.string.candidate_preview_input_mode) } else { suggestionAdapter.suggestions = emptyList() - binding.toggleCandidateListButton.text = "未入力時" + binding.toggleCandidateListButton.text = getString(R.string.candidate_preview_empty_mode) } + binding.candidatePreviewVisibilityButton.isVisible = isCandidateListVisible + updateCandidateTabPreview() + updateShortcutToolbarPreview() applyHeightDp(selectedHeightDp(), persist = false) + updateInspectorSummary() } private fun setSuggestionView() { - when (val columnNum = appPreference.candidate_column_preference) { + val columnNum = appPreference.getCandidateColumn(isLandscape = true) + clearItemDecorations(binding.candidateHeightSettingRecyclerview) + when (columnNum) { "1" -> { binding.candidateHeightSettingRecyclerview.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false) @@ -165,24 +374,36 @@ class CandidateHeightLandscapeSettingFragment : Fragment() { "2", "3" -> { val spanCount = columnNum.toInt() - val gridLayoutManager = GridLayoutManager( - requireContext(), spanCount, GridLayoutManager.HORIZONTAL, false - ) + val gridLayoutManager = + GridLayoutManager( + requireContext(), + spanCount, + GridLayoutManager.HORIZONTAL, + false + ).apply { + spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + return if (suggestionAdapter.isFullSpanItem(position)) { + spanCount + } else { + 1 + } + } + } + } + binding.candidateHeightSettingRecyclerview.layoutManager = gridLayoutManager val spacingInPixels = resources.getDimensionPixelSize(com.kazumaproject.core.R.dimen.grid_spacing) - - binding.candidateHeightSettingRecyclerview.layoutManager = - gridLayoutManager binding.candidateHeightSettingRecyclerview.addItemDecoration( - GridSpacingItemDecoration( - spanCount, spacingInPixels, true + CandidateHeightPreviewGridSpacingDecoration( + spanCount = spanCount, + spacing = spacingInPixels, + includeEdge = true ) ) } } - binding.candidateHeightSettingRecyclerview.apply { - adapter = suggestionAdapter - } + binding.candidateHeightSettingRecyclerview.adapter = suggestionAdapter } private fun applyCurrentDimensions() { @@ -191,16 +412,24 @@ class CandidateHeightLandscapeSettingFragment : Fragment() { private fun selectedHeightDp(): Int { return if (isCandidateListVisible) { - appPreference.candidate_view_height_dp_landscape ?: defaultHeightDp + appPreference.getCandidateVisibleHeightDp( + isLandscape = true, + column = appPreference.getCandidateColumn(isLandscape = true) + ) } else { - appPreference.candidate_view_empty_height_dp_landscape ?: defaultHeightDp + appPreference.candidate_view_empty_height_dp_landscape ?: 110 } } private fun saveSelectedHeightDp(heightDp: Int) { val clamped = heightDp.coerceIn(minHeightDp, maxHeightDp) if (isCandidateListVisible) { - appPreference.candidate_view_height_dp_landscape = clamped + appPreference.setCandidateVisibleHeightDp( + isLandscape = true, + column = appPreference.getCandidateColumn(isLandscape = true), + heightDp = clamped + ) + appPreference.syncActiveCandidateVisibleHeightToImePreference(isLandscape = true) } else { appPreference.candidate_view_empty_height_dp_landscape = clamped } @@ -208,25 +437,55 @@ class CandidateHeightLandscapeSettingFragment : Fragment() { private fun applyHeightDp(heightDp: Int, persist: Boolean) { val clamped = heightDp.coerceIn(minHeightDp, maxHeightDp) - val heightPx = (clamped * resources.displayMetrics.density).toInt() + val heightPx = clamped.dpToPx() updatePreviewHeightPx(heightPx) if (persist) { saveSelectedHeightDp(clamped) } syncHeightControls(clamped) + updateInspectorSummary() } private fun updatePreviewHeightPx(candidateHeightPx: Int) { - binding.candidateHeightSettingRecyclerview.layoutParams = - binding.candidateHeightSettingRecyclerview.layoutParams.apply { + val presentation = resolveCandidateHeightPreviewPresentation() + val keyboardHeightPx = keyboardPreviewHeightPx() + val independentToolbarHeightPx = presentation.independentShortcutToolbarHeightPx + binding.keyboardPreviewContainer.layoutParams = + (binding.keyboardPreviewContainer.layoutParams as FrameLayout.LayoutParams).apply { + gravity = Gravity.BOTTOM + height = keyboardHeightPx + bottomMargin = 0 + } + binding.candidatePreviewFrame.layoutParams = + (binding.candidatePreviewFrame.layoutParams as FrameLayout.LayoutParams).apply { + gravity = Gravity.BOTTOM height = candidateHeightPx + bottomMargin = keyboardHeightPx + } + binding.independentShortcutToolbarPreviewContainer.layoutParams = + (binding.independentShortcutToolbarPreviewContainer.layoutParams as FrameLayout.LayoutParams).apply { + gravity = Gravity.BOTTOM + height = independentToolbarHeightPx.coerceAtLeast(36.dpToPx()) + bottomMargin = keyboardHeightPx + candidateHeightPx + } + binding.candidateTabPreviewContainer.layoutParams = + (binding.candidateTabPreviewContainer.layoutParams as FrameLayout.LayoutParams).apply { + gravity = Gravity.TOP + height = presentation.candidateTabOffsetPx.coerceAtLeast(36.dpToPx()) } binding.candidateHeightSettingContent.layoutParams = binding.candidateHeightSettingContent.layoutParams.apply { - height = candidateHeightPx + height = presentation.candidateTabOffsetPx + + candidateHeightPx + + independentToolbarHeightPx + + keyboardHeightPx } } + private fun keyboardPreviewHeightPx(): Int { + return candidateKeyboardPreviewHeightPx(binding.keyboardPreviewContainer) + } + private fun syncHeightControls(heightDp: Int) { if (isSyncingHeightControls) return isSyncingHeightControls = true @@ -256,8 +515,7 @@ class CandidateHeightLandscapeSettingFragment : Fragment() { fromUser: Boolean ) { if (!fromUser || isSyncingHeightControls) return - val heightDp = minHeightDp + progress - applyHeightDp(heightDp, persist = true) + applyHeightDp(minHeightDp + progress, persist = true) } override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit @@ -281,21 +539,211 @@ class CandidateHeightLandscapeSettingFragment : Fragment() { private fun applyHeightFromEditText() { if (isSyncingHeightControls) return - val raw = binding.candidateHeightEditText.text?.toString()?.trim() - val value = raw?.toIntOrNull() + val value = binding.candidateHeightEditText.text?.toString()?.trim()?.toIntOrNull() if (value == null) { binding.candidateHeightInputLayout.error = getString(R.string.candidate_height_invalid_value) return } - val clamped = value.coerceIn(minHeightDp, maxHeightDp) - applyHeightDp(clamped, persist = true) + applyHeightDp(value.coerceIn(minHeightDp, maxHeightDp), persist = true) + } + + private fun setupCandidateLetterSizeSeekBar() { + binding.candidateLetterSizeSeekbar.max = + ((maxCandidateTextSize - minCandidateTextSize) * 10).roundToInt() + binding.candidateLetterSizeSeekbar.setOnSeekBarChangeListener( + object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged( + seekBar: SeekBar?, + progress: Int, + fromUser: Boolean + ) { + if (!fromUser || isSyncingLetterSizeControls) return + val newSize = minCandidateTextSize + progress / 10f + applyCandidateTextSize(newSize, persist = true) + } + + override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit + + override fun onStopTrackingTouch(seekBar: SeekBar?) = Unit + } + ) + } + + private fun setupCandidateLetterSizeEditText() { + binding.candidateLetterSizeEditText.setOnEditorActionListener { _, _, _ -> + applyCandidateLetterSizeFromEditText() + false + } + binding.candidateLetterSizeEditText.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + applyCandidateLetterSizeFromEditText() + } + } + } + + private fun applyCandidateLetterSizeFromEditText() { + if (isSyncingLetterSizeControls) return + val value = binding.candidateLetterSizeEditText.text?.toString()?.trim()?.toFloatOrNull() + if (value == null) { + binding.candidateLetterSizeInputLayout.error = + getString(R.string.candidate_letter_size_invalid_value) + return + } + applyCandidateTextSize(value.coerceIn(minCandidateTextSize, maxCandidateTextSize), persist = true) + } + + private fun applyCandidateTextSize(size: Float, persist: Boolean) { + val clamped = ((size.coerceIn(minCandidateTextSize, maxCandidateTextSize) * 10).roundToInt() / 10f) + suggestionAdapter.setCandidateTextSize(clamped) + if (persist) { + appPreference.candidate_letter_size = clamped + } + syncCandidateLetterSizeControls(clamped) + updateInspectorSummary() + } + + private fun syncCandidateLetterSizeControls(size: Float) { + if (isSyncingLetterSizeControls) return + isSyncingLetterSizeControls = true + try { + val clamped = size.coerceIn(minCandidateTextSize, maxCandidateTextSize) + binding.candidateLetterSizeSeekbar.progress = + ((clamped - minCandidateTextSize) * 10).roundToInt() + val text = String.format(Locale.US, "%.1f", clamped) + if (binding.candidateLetterSizeEditText.text?.toString() != text) { + binding.candidateLetterSizeEditText.setText(text) + binding.candidateLetterSizeEditText.setSelection(text.length) + } + binding.candidateLetterSizeInputLayout.error = null + } finally { + isSyncingLetterSizeControls = false + } + } + + private fun setupDefaultHeightControls() { + binding.saveDefaultsButton.setOnClickListener { + saveDefaultHeightsFromInputs() + } + binding.useCurrentDefaultsButton.setOnClickListener { + appPreference.copyCandidateVisibleHeightsToUserDefaults(isLandscape = true) + syncDefaultHeightControls() + } + binding.restoreFactoryDefaultsButton.setOnClickListener { + appPreference.resetCandidateDefaultVisibleHeightsToFactoryDefaults(isLandscape = true) + syncDefaultHeightControls() + } + listOf( + binding.defaultHeightOneEditText, + binding.defaultHeightTwoEditText, + binding.defaultHeightThreeEditText + ).forEach { editText -> + editText.setOnEditorActionListener { _, _, _ -> + saveDefaultHeightsFromInputs() + false + } + } + } + + private fun saveDefaultHeightsFromInputs(): Boolean { + if (isSyncingDefaultHeightControls) return false + val one = readDefaultHeightInput( + binding.defaultHeightOneInputLayout, + binding.defaultHeightOneEditText + ) ?: return false + val two = readDefaultHeightInput( + binding.defaultHeightTwoInputLayout, + binding.defaultHeightTwoEditText + ) ?: return false + val three = readDefaultHeightInput( + binding.defaultHeightThreeInputLayout, + binding.defaultHeightThreeEditText + ) ?: return false + + appPreference.setCandidateDefaultVisibleHeightDp( + isLandscape = true, + column = "1", + heightDp = one + ) + appPreference.setCandidateDefaultVisibleHeightDp( + isLandscape = true, + column = "2", + heightDp = two + ) + appPreference.setCandidateDefaultVisibleHeightDp( + isLandscape = true, + column = "3", + heightDp = three + ) + syncDefaultHeightControls() + return true + } + + private fun readDefaultHeightInput( + inputLayout: TextInputLayout, + editText: TextInputEditText + ): Int? { + val value = editText.text?.toString()?.trim()?.toIntOrNull() + if (value == null) { + inputLayout.error = getString(R.string.candidate_height_invalid_value) + return null + } + inputLayout.error = null + return value.coerceIn(minHeightDp, maxHeightDp) + } + + private fun syncDefaultHeightControls() { + if (isSyncingDefaultHeightControls) return + isSyncingDefaultHeightControls = true + try { + setDefaultHeightText( + binding.defaultHeightOneInputLayout, + binding.defaultHeightOneEditText, + appPreference.getCandidateDefaultVisibleHeightDp(isLandscape = true, column = "1") + ) + setDefaultHeightText( + binding.defaultHeightTwoInputLayout, + binding.defaultHeightTwoEditText, + appPreference.getCandidateDefaultVisibleHeightDp(isLandscape = true, column = "2") + ) + setDefaultHeightText( + binding.defaultHeightThreeInputLayout, + binding.defaultHeightThreeEditText, + appPreference.getCandidateDefaultVisibleHeightDp(isLandscape = true, column = "3") + ) + } finally { + isSyncingDefaultHeightControls = false + } + } + + private fun setDefaultHeightText( + inputLayout: TextInputLayout, + editText: TextInputEditText, + heightDp: Int + ) { + val text = heightDp.coerceIn(minHeightDp, maxHeightDp).toString() + if (editText.text?.toString() != text) { + editText.setText(text) + editText.setSelection(text.length) + } + inputLayout.error = null + } + + private fun updateInspectorSummary(heightDp: Int = selectedHeightDp()) { + val textSize = appPreference.candidate_letter_size ?: defaultCandidateTextSize + binding.inspectorSummaryText.text = getString( + R.string.candidate_height_sheet_summary_format, + appPreference.getCandidateColumn(isLandscape = true), + heightDp.coerceIn(minHeightDp, maxHeightDp).toString(), + String.format(Locale.US, "%.1f", textSize) + ) } @SuppressLint("ClickableViewAccessibility") private fun setupResizeHandle() { var initialY = 0f var initialHeight = 0 + val density = resources.displayMetrics.density val minHeightPx = minHeightDp * density val maxHeightPx = maxHeightDp * density @@ -303,21 +751,32 @@ class CandidateHeightLandscapeSettingFragment : Fragment() { binding.handleTop.setOnTouchListener { _, event -> when (event.action) { MotionEvent.ACTION_DOWN -> { + binding.handleTop.parent?.requestDisallowInterceptTouchEvent(true) + binding.candidateControlsScroll.requestDisallowInterceptTouchEvent(true) initialY = event.rawY - initialHeight = binding.candidateHeightSettingRecyclerview.height + initialHeight = binding.candidatePreviewFrame.height true } MotionEvent.ACTION_MOVE -> { + binding.handleTop.parent?.requestDisallowInterceptTouchEvent(true) + binding.candidateControlsScroll.requestDisallowInterceptTouchEvent(true) val deltaY = event.rawY - initialY val newHeight = (initialHeight - deltaY).coerceIn(minHeightPx, maxHeightPx) - updatePreviewHeightPx(newHeight.toInt()) - binding.candidateHeightSettingRecyclerview.requestLayout() + val currentHeightDp = + (newHeight / density).roundToInt().coerceIn(minHeightDp, maxHeightDp) + updatePreviewHeightPx(currentHeightDp.dpToPx()) + syncHeightControls(currentHeightDp) + updateInspectorSummary(currentHeightDp) + binding.candidatePreviewFrame.requestLayout() binding.candidateHeightSettingContent.requestLayout() true } - MotionEvent.ACTION_UP -> { + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> { + binding.handleTop.parent?.requestDisallowInterceptTouchEvent(false) + binding.candidateControlsScroll.requestDisallowInterceptTouchEvent(false) val finalHeightDp = saveHeightPreference() applyHeightDp(finalHeightDp, persist = false) true @@ -330,20 +789,229 @@ class CandidateHeightLandscapeSettingFragment : Fragment() { private fun saveHeightPreference(): Int { val density = resources.displayMetrics.density - val heightPx = binding.candidateHeightSettingRecyclerview.layoutParams.height + val heightPx = binding.candidatePreviewFrame.layoutParams.height .takeIf { it > 0 } - ?: binding.candidateHeightSettingRecyclerview.height + ?: binding.candidatePreviewFrame.height val finalHeightDp = (heightPx / density).roundToInt().coerceIn(minHeightDp, maxHeightDp) saveSelectedHeightDp(finalHeightDp) - // Save to landscape-specific preferences - if (isCandidateListVisible) { - Timber.d("saveHeightPreference landscape (with candidates): $finalHeightDp dp") - } else { - Timber.d("saveHeightPreference landscape (empty): $finalHeightDp dp") - } + Timber.d( + "saveHeightPreference landscape (%s): %d dp", + if (isCandidateListVisible) "with candidates" else "empty", + finalHeightDp + ) return finalHeightDp } + + private fun syncColumnControls() { + if (isSyncingColumnControls) return + isSyncingColumnControls = true + try { + binding.candidateColumnToggleGroup.check( + when (appPreference.getCandidateColumn(isLandscape = true)) { + "2" -> R.id.candidate_column_two_button + "3" -> R.id.candidate_column_three_button + else -> R.id.candidate_column_one_button + } + ) + } finally { + isSyncingColumnControls = false + } + } + + private fun columnForButtonId(id: Int): String? = + when (id) { + R.id.candidate_column_one_button -> "1" + R.id.candidate_column_two_button -> "2" + R.id.candidate_column_three_button -> "3" + else -> null + } + + private fun updateCandidateTabPreview() { + val presentation = resolveCandidateHeightPreviewPresentation() + binding.candidateTabPreviewContainer.isVisible = presentation.showCandidateTab + if (!presentation.showCandidateTab) return + + binding.candidateTabPreviewContainer.removeAllViews() + val tabs = runCatching { appPreference.candidate_tab_order } + .getOrDefault(listOf(CandidateTab.PREDICTION, CandidateTab.CONVERSION, CandidateTab.EISUKANA)) + tabs.forEachIndexed { index, tab -> + binding.candidateTabPreviewContainer.addView( + previewTabView(label = tab.previewLabel(), selected = index == 0), + LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f) + ) + } + } + + private fun updateShortcutToolbarPreview() { + val presentation = resolveCandidateHeightPreviewPresentation() + when { + presentation.showIndependentShortcutToolbar -> { + binding.independentShortcutToolbarPreviewContainer.isVisible = true + populateShortcutToolbarPreview(binding.independentShortcutToolbarPreviewContainer) + } + + presentation.reserveIndependentShortcutToolbarSpace -> { + binding.independentShortcutToolbarPreviewContainer.isInvisible = true + } + + else -> { + binding.independentShortcutToolbarPreviewContainer.isVisible = false + } + } + suggestionAdapter.setShortcutItems(previewShortcutItems()) + suggestionAdapter.setIntegratedShortcutVisibility(presentation.showIntegratedShortcut) + } + + private data class CandidateHeightPreviewPresentation( + val showCandidateTab: Boolean, + val showIndependentShortcutToolbar: Boolean, + val reserveIndependentShortcutToolbarSpace: Boolean, + val showIntegratedShortcut: Boolean, + val candidateTabOffsetPx: Int, + val independentShortcutToolbarHeightPx: Int + ) + + private fun resolveCandidateHeightPreviewPresentation(): CandidateHeightPreviewPresentation { + val inputStringEmpty = !isCandidateListVisible + val tailEmpty = true + val clipboardPreviewShown = false + val selectedTextGemmaActionsShown = false + val suggestionsEmpty = !isCandidateListVisible + val customLayoutPickerShown = false + val presentation = CandidateStripPresentationPolicy.resolve( + CandidateStripPresentationState( + candidateTabVisible = appPreference.candidate_tab_preference, + candidatesShown = isCandidateListVisible, + resetCandidateTabSelection = false, + shortcutToolbarVisible = appPreference.shortcut_toolbar_visibility_preference, + shortcutToolbarIntegratedInSuggestion = + appPreference.shortcut_toolbar_integrated_in_suggestion_preference, + inputStringEmpty = inputStringEmpty, + tailEmpty = tailEmpty, + clipboardPreviewShown = clipboardPreviewShown, + selectedTextGemmaActionsShown = selectedTextGemmaActionsShown, + suggestionsEmpty = suggestionsEmpty, + customLayoutPickerShown = customLayoutPickerShown, + symbolKeyboardShown = false, + shortcutToolbarHiddenForCandidates = false + ) + ) + val independentHeightPx = + if ( + presentation.showIndependentShortcutToolbar || + presentation.reserveIndependentShortcutToolbarSpace + ) { + 36.dpToPx() + } else { + 0 + } + return CandidateHeightPreviewPresentation( + showCandidateTab = presentation.showCandidateTab, + showIndependentShortcutToolbar = presentation.showIndependentShortcutToolbar, + reserveIndependentShortcutToolbarSpace = + presentation.reserveIndependentShortcutToolbarSpace, + showIntegratedShortcut = presentation.showIntegratedShortcut, + candidateTabOffsetPx = if (presentation.showCandidateTab) 36.dpToPx() else 0, + independentShortcutToolbarHeightPx = independentHeightPx + ) + } + + private fun previewShortcutItems(): List = + listOf( + ShortcutType.SETTINGS, + ShortcutType.EMOJI, + ShortcutType.TEMPLATE, + ShortcutType.KEYBOARD_PICKER, + ShortcutType.PASTE + ) + + private fun populateShortcutToolbarPreview(container: LinearLayout) { + container.removeAllViews() + previewShortcutItems().forEach { shortcut -> + val button = AppCompatImageButton(requireContext()).apply { + setImageResource(shortcut.iconResId) + imageTintList = ColorStateList.valueOf(resolveThemeColor(MaterialR.attr.colorOnSurface)) + background = null + contentDescription = shortcut.description + isClickable = false + isFocusable = false + setPadding(8.dpToPx(), 6.dpToPx(), 8.dpToPx(), 6.dpToPx()) + } + container.addView( + button, + LinearLayout.LayoutParams(42.dpToPx(), ViewGroup.LayoutParams.MATCH_PARENT) + ) + } + } + + private fun setupKeyboardPreview() { + renderCandidateKeyboardPreview( + fragment = this, + appPreference = appPreference, + keyboardRepository = keyboardRepository, + sumireSpecialKeyRepository = sumireSpecialKeyRepository, + views = CandidateKeyboardPreviewViews( + container = binding.keyboardPreviewContainer, + tenKey = binding.candidateHeightSettingTenkeyPreview, + qwerty = binding.candidateHeightSettingQwertyPreview, + flick = binding.candidateHeightSettingFlickPreview + ), + isLandscape = true, + onPreviewLayoutChanged = ::applyCurrentDimensions + ) + } + + private fun previewTabView(label: String, selected: Boolean): MaterialTextView { + return MaterialTextView(requireContext()).apply { + text = label + gravity = Gravity.CENTER + textSize = 13f + setTextColor( + resolveThemeColor( + if (selected) MaterialR.attr.colorOnPrimary else MaterialR.attr.colorOnSurface + ) + ) + background = roundedBackground( + fillColor = resolveThemeColor( + if (selected) AppCompatR.attr.colorPrimary else MaterialR.attr.colorSurfaceVariant + ), + radiusDp = 6 + ) + } + } + + private fun roundedBackground( + fillColor: Int, + strokeColor: Int? = null, + radiusDp: Int + ): GradientDrawable { + return GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + cornerRadius = radiusDp.dpToPx().toFloat() + setColor(fillColor) + strokeColor?.let { setStroke(1.dpToPx(), it) } + } + } + + private fun CandidateTab.previewLabel(): String = + when (this) { + CandidateTab.PREDICTION -> "予測" + CandidateTab.CONVERSION -> "変換" + CandidateTab.EISUKANA -> "英数カナ" + } + + private fun resolveThemeColor(@AttrRes attr: Int): Int { + val typedArray = requireContext().obtainStyledAttributes(intArrayOf(attr)) + return try { + typedArray.getColor(0, Color.TRANSPARENT) + } finally { + typedArray.recycle() + } + } + + private fun Int.dpToPx(): Int = + (this * resources.displayMetrics.density).roundToInt() } diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_setting/CandidateHeightPreviewUtils.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_setting/CandidateHeightPreviewUtils.kt new file mode 100644 index 00000000..3e4aeea2 --- /dev/null +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_setting/CandidateHeightPreviewUtils.kt @@ -0,0 +1,802 @@ +package com.kazumaproject.markdownhelperkeyboard.setting_activity.ui.candidate_view_height_setting + +import android.content.Context +import android.content.res.Configuration +import android.graphics.Color +import android.graphics.Rect +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.LinearLayout +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.window.layout.WindowMetricsCalculator +import com.google.android.material.color.DynamicColors +import com.kazumaproject.core.data.popup.FlickPopupViewStyleSet +import com.kazumaproject.core.data.popup.PopupViewStyle +import com.kazumaproject.core.data.popup.QwertyPopupViewStyleSet +import com.kazumaproject.core.domain.key.Key +import com.kazumaproject.core.domain.listener.FlickListener +import com.kazumaproject.core.domain.listener.LongPressListener +import com.kazumaproject.core.domain.state.GestureType +import com.kazumaproject.custom_keyboard.data.FlickDirection +import com.kazumaproject.custom_keyboard.data.KeyAction +import com.kazumaproject.custom_keyboard.data.KeyActionMapper +import com.kazumaproject.custom_keyboard.data.KeyboardInputMode +import com.kazumaproject.custom_keyboard.data.KeyboardLayout +import com.kazumaproject.custom_keyboard.layout.KeyboardDefaultLayouts +import com.kazumaproject.custom_keyboard.view.FlickKeyboardView +import com.kazumaproject.markdownhelperkeyboard.ime_service.resolveInitialCustomKeyboardSelection +import com.kazumaproject.markdownhelperkeyboard.ime_service.state.KeyboardType +import com.kazumaproject.markdownhelperkeyboard.repository.KeyboardRepository +import com.kazumaproject.markdownhelperkeyboard.setting_activity.AppPreference +import com.kazumaproject.markdownhelperkeyboard.setting_activity.circular_slot.CircularSlotActionApplier +import com.kazumaproject.markdownhelperkeyboard.sumire_special_key.SumireSpecialKeyActionDisplayMetadata +import com.kazumaproject.markdownhelperkeyboard.sumire_special_key.SumireSpecialKeyActionDisplayOverrideApplier +import com.kazumaproject.markdownhelperkeyboard.sumire_special_key.SumireSpecialKeyActionResolver +import com.kazumaproject.markdownhelperkeyboard.sumire_special_key.SumireSpecialKeyPlacementOverrideApplier +import com.kazumaproject.markdownhelperkeyboard.sumire_special_key.SumireSpecialKeyRepository +import com.kazumaproject.qwerty_keyboard.ui.QWERTYKeyboardView +import com.kazumaproject.tenkey.TenKey +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.kazumaproject.markdownhelperkeyboard.converter.candidate.Candidate +import timber.log.Timber +import kotlin.math.roundToInt + +internal fun createCandidateHeightPreviewCandidates(): List { + return listOf( + "変換" to "へんかん", + "変換候補" to "へんかんこうほ", + "変換する" to "へんかんする", + "日本語" to "にほんご", + "入力" to "にゅうりょく", + "候補欄" to "こうほらん", + "設定" to "せってい", + "予測" to "よそく", + "学習なし" to "がくしゅうなし", + "オフライン" to "おふらいん", + "キーボード" to "きーぼーど", + "Markdown" to "まーくだうん" + ).mapIndexed { index, (text, yomi) -> + Candidate( + string = text, + type = if (index == 0) 9.toByte() else 1.toByte(), + length = text.length.toUByte(), + score = 4000 - index, + yomi = yomi, + leftId = 0.toShort(), + rightId = 0.toShort() + ) + } +} + +internal fun clearItemDecorations(recyclerView: RecyclerView) { + while (recyclerView.itemDecorationCount > 0) { + recyclerView.removeItemDecorationAt(0) + } +} + +internal class CandidateHeightPreviewGridSpacingDecoration( + private val spanCount: Int, + private val spacing: Int, + private val includeEdge: Boolean +) : RecyclerView.ItemDecoration() { + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + val position = parent.getChildAdapterPosition(view) + if (position < 0) { + outRect.set(0, 0, 0, 0) + return + } + + val layoutManager = parent.layoutManager as? GridLayoutManager ?: return + val layoutParams = view.layoutParams as GridLayoutManager.LayoutParams + + if (layoutManager.orientation == GridLayoutManager.HORIZONTAL) { + val row = layoutParams.spanIndex + val column = position / spanCount + if (includeEdge) { + outRect.top = spacing - row * spacing / spanCount + outRect.bottom = (row + 1) * spacing / spanCount + if (column == 0) { + outRect.left = spacing + } + outRect.right = spacing + } else { + outRect.top = row * spacing / spanCount + outRect.bottom = spacing - (row + 1) * spacing / spanCount + if (column > 0) { + outRect.left = spacing + } + } + } + } +} + +internal data class CandidateKeyboardPreviewViews( + val container: FrameLayout, + val tenKey: TenKey, + val qwerty: QWERTYKeyboardView, + val flick: FlickKeyboardView +) + +internal fun renderCandidateKeyboardPreview( + fragment: Fragment, + appPreference: AppPreference, + keyboardRepository: KeyboardRepository, + sumireSpecialKeyRepository: SumireSpecialKeyRepository, + views: CandidateKeyboardPreviewViews, + isLandscape: Boolean, + onPreviewLayoutChanged: () -> Unit +) { + val previewKeyboardType = appPreference.keyboard_order.firstOrNull() ?: KeyboardType.TENKEY + applyCandidateKeyboardPreviewLayout( + fragment = fragment, + appPreference = appPreference, + container = views.container, + type = previewKeyboardType, + isLandscape = isLandscape + ) + onPreviewLayoutChanged() + + views.tenKey.isVisible = false + views.qwerty.isVisible = false + views.flick.isVisible = false + + when (previewKeyboardType) { + KeyboardType.CUSTOM -> { + views.flick.isVisible = true + configureFlickKeyboardPreview(fragment.requireContext(), appPreference, views.flick) + fragment.viewLifecycleOwner.lifecycleScope.launch { + val customLayout = withContext(Dispatchers.IO) { + loadPreviewCustomKeyboardLayout(appPreference, keyboardRepository) + } + views.flick.clearSumireSpecialKeyActionResolver() + if (customLayout == null) { + val fallbackType = + appPreference.keyboard_order.firstOrNull { it != KeyboardType.CUSTOM } + if (fallbackType != null) { + renderNonCustomKeyboardPreviewType( + fragment = fragment, + appPreference = appPreference, + sumireSpecialKeyRepository = sumireSpecialKeyRepository, + views = views, + type = fallbackType, + isLandscape = isLandscape, + onPreviewLayoutChanged = onPreviewLayoutChanged + ) + } else { + views.flick.isVisible = false + } + return@launch + } + val finalLayout = applyDeleteKeyFlickPreferences(appPreference, customLayout.layout) + views.flick.setKeyboard(finalLayout) + syncCustomKeyboardToggleKeyIcons( + flickView = views.flick, + isDirectMode = customLayout.isDirectMode, + isRomaji = customLayout.isRomaji, + isShiftPressed = false, + isCapLock = false + ) + } + } + + else -> { + renderNonCustomKeyboardPreviewType( + fragment = fragment, + appPreference = appPreference, + sumireSpecialKeyRepository = sumireSpecialKeyRepository, + views = views, + type = previewKeyboardType, + isLandscape = isLandscape, + onPreviewLayoutChanged = onPreviewLayoutChanged + ) + } + } +} + +private fun renderNonCustomKeyboardPreviewType( + fragment: Fragment, + appPreference: AppPreference, + sumireSpecialKeyRepository: SumireSpecialKeyRepository, + views: CandidateKeyboardPreviewViews, + type: KeyboardType, + isLandscape: Boolean, + onPreviewLayoutChanged: () -> Unit +) { + applyCandidateKeyboardPreviewLayout( + fragment = fragment, + appPreference = appPreference, + container = views.container, + type = type, + isLandscape = isLandscape + ) + onPreviewLayoutChanged() + + views.tenKey.isVisible = false + views.qwerty.isVisible = false + views.flick.isVisible = false + + when (type) { + KeyboardType.TENKEY -> { + views.tenKey.isVisible = true + configureTenKeyPreview(fragment.requireContext(), appPreference, views.tenKey) + } + + KeyboardType.QWERTY, + KeyboardType.ROMAJI -> { + views.qwerty.isVisible = true + configureQwertyPreview( + context = fragment.requireContext(), + appPreference = appPreference, + qwertyView = views.qwerty, + isRomaji = type == KeyboardType.ROMAJI + ) + } + + KeyboardType.SUMIRE -> { + views.flick.isVisible = true + configureFlickKeyboardPreview(fragment.requireContext(), appPreference, views.flick) + fragment.viewLifecycleOwner.lifecycleScope.launch { + val layoutType = appPreference.sumire_input_method + val inputMode = KeyboardInputMode.HIRAGANA + val actionOverrides = withContext(Dispatchers.IO) { + sumireSpecialKeyRepository.observeAllActionOverrides().first() + } + val placementOverrides = withContext(Dispatchers.IO) { + sumireSpecialKeyRepository.observeAllPlacementOverrides().first() + } + val layout = createPreviewSumireKeyboardLayout( + context = fragment.requireContext(), + appPreference = appPreference, + inputMode = inputMode, + layoutType = layoutType, + actionOverrides = actionOverrides, + placementOverrides = placementOverrides + ) + views.flick.setSumireSpecialKeyActionResolver( + resolver = SumireSpecialKeyActionResolver(actionOverrides)::resolve, + layoutType = layoutType, + inputMode = inputMode.name + ) + views.flick.setKeyboard(layout) + } + } + + KeyboardType.CUSTOM -> Unit + } +} + +internal fun candidateKeyboardPreviewHeightPx(container: FrameLayout): Int { + val layoutParams = container.layoutParams + return layoutParams.height.takeIf { it > 0 } ?: container.context.dpToPx(220) +} + +private data class PreviewKeyboardLayoutConfig( + val heightDp: Int, + val widthPercent: Int, + val bottomMarginDp: Int, + val positionIsEnd: Boolean, + val startMarginDp: Int, + val endMarginDp: Int +) + +private data class PreviewCustomKeyboardLayout( + val layout: KeyboardLayout, + val isRomaji: Boolean, + val isDirectMode: Boolean +) + +private fun applyCandidateKeyboardPreviewLayout( + fragment: Fragment, + appPreference: AppPreference, + container: FrameLayout, + type: KeyboardType, + isLandscape: Boolean +) { + val config = previewKeyboardLayoutConfig( + appPreference = appPreference, + type = type, + isLandscape = isLandscape + ) + val screenWidth = WindowMetricsCalculator.getOrCreate() + .computeCurrentWindowMetrics(fragment.requireActivity()).bounds.width() + val widthPercent = config.widthPercent.coerceIn(32, 100) + val widthPx = if (widthPercent >= 98) { + ViewGroup.LayoutParams.MATCH_PARENT + } else { + (screenWidth * (widthPercent / 100f)).roundToInt() + } + val heightPx = config.heightDp.coerceIn(100, 420).let(container.context::dpToPx) + val horizontalGravity = if (config.positionIsEnd) Gravity.END else Gravity.START + val startMarginPx = if (config.positionIsEnd) 0 else container.context.dpToPx(config.startMarginDp) + val endMarginPx = if (config.positionIsEnd) container.context.dpToPx(config.endMarginDp) else 0 + val bottomMarginPx = container.context.dpToPx(config.bottomMarginDp) + val layoutParams = when (val params = container.layoutParams) { + is FrameLayout.LayoutParams -> params.apply { + height = heightPx + width = widthPx + gravity = Gravity.BOTTOM or horizontalGravity + marginStart = startMarginPx + marginEnd = endMarginPx + bottomMargin = bottomMarginPx + } + + is LinearLayout.LayoutParams -> params.apply { + height = heightPx + width = widthPx + gravity = horizontalGravity + marginStart = startMarginPx + marginEnd = endMarginPx + bottomMargin = bottomMarginPx + } + + is ViewGroup.MarginLayoutParams -> FrameLayout.LayoutParams(params).apply { + height = heightPx + width = widthPx + gravity = Gravity.BOTTOM or horizontalGravity + marginStart = startMarginPx + marginEnd = endMarginPx + bottomMargin = bottomMarginPx + } + + else -> FrameLayout.LayoutParams(widthPx, heightPx, Gravity.BOTTOM or horizontalGravity).apply { + marginStart = startMarginPx + marginEnd = endMarginPx + bottomMargin = bottomMarginPx + } + } + container.layoutParams = layoutParams +} + +private fun previewKeyboardLayoutConfig( + appPreference: AppPreference, + type: KeyboardType, + isLandscape: Boolean +): PreviewKeyboardLayoutConfig { + val useQwertySize = type == KeyboardType.QWERTY || type == KeyboardType.ROMAJI + return if (useQwertySize) { + PreviewKeyboardLayoutConfig( + heightDp = if (isLandscape) { + appPreference.qwerty_keyboard_height_landscape ?: 220 + } else { + appPreference.qwerty_keyboard_height ?: 220 + }, + widthPercent = if (isLandscape) { + appPreference.qwerty_keyboard_width_landscape ?: 100 + } else { + appPreference.qwerty_keyboard_width ?: 100 + }, + bottomMarginDp = if (isLandscape) { + appPreference.qwerty_keyboard_vertical_margin_bottom_landscape ?: 0 + } else { + appPreference.qwerty_keyboard_vertical_margin_bottom ?: 0 + }, + positionIsEnd = if (isLandscape) { + appPreference.qwerty_keyboard_position_landscape ?: true + } else { + appPreference.qwerty_keyboard_position ?: true + }, + startMarginDp = if (isLandscape) { + appPreference.qwerty_keyboard_margin_start_dp_landscape ?: 0 + } else { + appPreference.qwerty_keyboard_margin_start_dp ?: 0 + }, + endMarginDp = if (isLandscape) { + appPreference.qwerty_keyboard_margin_end_dp_landscape ?: 0 + } else { + appPreference.qwerty_keyboard_margin_end_dp ?: 0 + } + ) + } else { + PreviewKeyboardLayoutConfig( + heightDp = if (isLandscape) { + appPreference.keyboard_height_landscape ?: 220 + } else { + appPreference.keyboard_height ?: 220 + }, + widthPercent = if (isLandscape) { + appPreference.keyboard_width_landscape ?: 100 + } else { + appPreference.keyboard_width ?: 100 + }, + bottomMarginDp = if (isLandscape) { + appPreference.keyboard_vertical_margin_bottom_landscape ?: 0 + } else { + appPreference.keyboard_vertical_margin_bottom ?: 0 + }, + positionIsEnd = if (isLandscape) { + appPreference.keyboard_position_landscape ?: true + } else { + appPreference.keyboard_position ?: true + }, + startMarginDp = if (isLandscape) { + appPreference.keyboard_margin_start_dp_landscape ?: 0 + } else { + appPreference.keyboard_margin_start_dp ?: 0 + }, + endMarginDp = if (isLandscape) { + appPreference.keyboard_margin_end_dp_landscape ?: 0 + } else { + appPreference.keyboard_margin_end_dp ?: 0 + } + ) + } +} + +private fun configureTenKeyPreview( + context: Context, + appPreference: AppPreference, + tenKey: TenKey +) { + tenKey.applyKeyboardTheme( + themeMode = appPreference.theme_mode, + currentNightMode = currentNightMode(context), + isDynamicColorEnabled = DynamicColors.isDynamicColorAvailable(), + customBgColor = appPreference.custom_theme_bg_color, + customKeyColor = appPreference.custom_theme_key_color, + customSpecialKeyColor = appPreference.custom_theme_special_key_color, + customKeyTextColor = appPreference.custom_theme_key_text_color, + customSpecialKeyTextColor = appPreference.custom_theme_special_key_text_color, + liquidGlassEnable = appPreference.liquid_glass_preference, + customBorderEnable = appPreference.custom_theme_border_enable, + customBorderColor = appPreference.custom_theme_border_color, + liquidGlassKeyAlphaEnable = appPreference.liquid_glass_key_alpha, + borderWidth = appPreference.custom_theme_border_width + ) + tenKey.setFlickSensitivityValue(appPreference.flick_sensitivity_preference ?: 100) + tenKey.setLongPressTimeout((appPreference.long_press_timeout_preference ?: 300).toLong()) + tenKey.applyPopupViewStyle( + PopupViewStyle( + sizeScalePercent = appPreference.tenkey_popup_size_scale_percent ?: 100, + textSizeSp = appPreference.tenkey_popup_text_size_sp ?: 28.0f + ) + ) + tenKey.setUseThreeStateKeyboard(appPreference.tenkey_use_three_state_keyboard_preference) + tenKey.setUseQwertyNumberWhenThreeStateOff( + appPreference.tenkey_switch_number_to_qwerty_number_preference + ) + tenKey.setKeyLetterSize((appPreference.key_letter_size ?: 0.0f) + 17f) + tenKey.setKeyLetterSizeDelta((appPreference.key_letter_size ?: 0.0f).toInt()) + tenKey.setKeySizeScale( + appPreference.tenkey_key_width_scale_percent ?: 100, + appPreference.tenkey_key_height_scale_percent ?: 100 + ) + tenKey.setLanguageEnableKeyState(appPreference.tenkey_show_language_button_preference) + tenKey.setFlickGuideEnabled(appPreference.tenkey_keymap_guide_layout ?: false) + tenKey.setOnQwertyNumberModeRequestedListener(null) + tenKey.setOnFlickListener(object : FlickListener { + override fun onFlick(gestureType: GestureType, key: Key, char: Char?) = Unit + }) + tenKey.setOnLongPressListener(object : LongPressListener { + override fun onLongPress(key: Key) = Unit + }) + tenKey.isClickable = false + tenKey.isFocusable = false + tenKey.setOnTouchListener { _, _ -> true } +} + +private fun configureQwertyPreview( + context: Context, + appPreference: AppPreference, + qwertyView: QWERTYKeyboardView, + isRomaji: Boolean +) { + qwertyView.applyKeyboardTheme( + themeMode = appPreference.theme_mode, + currentNightMode = currentNightMode(context), + isDynamicColorEnabled = DynamicColors.isDynamicColorAvailable(), + customBgColor = appPreference.custom_theme_bg_color, + customKeyColor = appPreference.custom_theme_key_color, + customSpecialKeyColor = appPreference.custom_theme_special_key_color, + customKeyTextColor = appPreference.custom_theme_key_text_color, + customSpecialKeyTextColor = appPreference.custom_theme_special_key_text_color, + liquidGlassEnable = appPreference.liquid_glass_preference, + customBorderEnable = appPreference.custom_theme_border_enable, + customBorderColor = appPreference.custom_theme_border_color, + liquidGlassKeyAlphaEnable = appPreference.liquid_glass_key_alpha, + borderWidth = appPreference.custom_theme_border_width + ) + qwertyView.setLongPressTimeout((appPreference.long_press_timeout_preference ?: 300).toLong()) + qwertyView.applyPopupViewStyleSet( + QwertyPopupViewStyleSet( + keyPreview = PopupViewStyle( + sizeScalePercent = appPreference.qwerty_key_preview_popup_size_scale_percent ?: 100, + textSizeSp = appPreference.qwerty_key_preview_popup_text_size_sp ?: 28.0f + ), + variation = PopupViewStyle( + sizeScalePercent = appPreference.qwerty_variation_popup_size_scale_percent ?: 100, + textSizeSp = appPreference.qwerty_variation_popup_text_size_sp ?: 22.0f + ) + ) + ) + qwertyView.setSpecialKeyVisibility( + showCursors = appPreference.qwerty_show_cursor_buttons ?: false, + showSwitchKey = appPreference.qwerty_show_ime_button ?: true, + showKutouten = appPreference.qwerty_show_kutouten_buttons ?: false, + showEmojiKey = appPreference.qwerty_show_emoji_button ?: false + ) + qwertyView.setRomajiEnglishSwitchKeyTextWithStyle(true) + qwertyView.updateSymbolKeymapState(appPreference.qwerty_show_keymap_symbols ?: false) + qwertyView.updateNumberKeyState(appPreference.qwerty_show_number_buttons ?: false) + qwertyView.setPopUpViewState(appPreference.qwerty_show_popup_window ?: true) + qwertyView.setFlickUpDetectionEnabled(appPreference.qwerty_enable_flick_up_preference ?: false) + qwertyView.setFlickDownDetectionEnabled(appPreference.qwerty_enable_flick_down_preference ?: false) + qwertyView.setNumberKeyFlickUpChars(appPreference.getQwertyNumberKeyFlickUpChars()) + qwertyView.setNumberKeyFlickDownChars(appPreference.getQwertyNumberKeyFlickDownChars()) + qwertyView.setNumberSwitchKeyTextStyle( + excludeNumber = appPreference.qwerty_switch_number_key_without_number_preference + ) + qwertyView.setSwitchNumberLayoutKeyVisibility(false) + qwertyView.setDeleteLeftFlickEnabled(appPreference.delete_key_left_flick_preference) + qwertyView.setDeleteUpFlickEnabled(appPreference.delete_key_up_flick_preference) + qwertyView.setDeleteDownFlickEnabled(appPreference.delete_key_down_flick_preference) + qwertyView.setKeyMargins( + verticalDp = appPreference.qwerty_key_vertical_margin ?: 5.0f, + horizontalGapDp = appPreference.qwerty_key_horizontal_gap ?: 2.0f, + indentLargeDp = appPreference.qwerty_key_indent_large ?: 23.0f, + indentSmallDp = appPreference.qwerty_key_indent_small ?: 9.0f, + sideMarginDp = appPreference.qwerty_key_side_margin ?: 4.0f, + textSizeSp = appPreference.qwerty_key_text_size ?: 18.0f, + specialTextSizeSp = appPreference.qwerty_special_key_text_size ?: 12.0f, + specialIconSizeDp = appPreference.qwerty_special_key_icon_size ?: 18.0f + ) + if (isRomaji) { + qwertyView.setRomajiKeyboard(context.getString(com.kazumaproject.core.R.string.return_japanese)) + qwertyView.setRomajiEnglishSwitchKeyVisibility(true) + } else { + qwertyView.resetQWERTYKeyboard(context.getString(com.kazumaproject.core.R.string.return_english)) + qwertyView.setRomajiEnglishSwitchKeyVisibility(false) + } + qwertyView.isClickable = false + qwertyView.isFocusable = false + qwertyView.setOnTouchListener { _, _ -> true } +} + +private fun configureFlickKeyboardPreview( + context: Context, + appPreference: AppPreference, + flickView: FlickKeyboardView +) { + flickView.setPopupWindowAnchorProvider(null) + flickView.applyKeyboardTheme( + themeMode = appPreference.theme_mode, + currentNightMode = currentNightMode(context), + isDynamicColorEnabled = DynamicColors.isDynamicColorAvailable(), + customBgColor = appPreference.custom_theme_bg_color, + customKeyColor = appPreference.custom_theme_key_color, + customSpecialKeyColor = appPreference.custom_theme_special_key_color, + customKeyTextColor = appPreference.custom_theme_key_text_color, + customSpecialKeyTextColor = appPreference.custom_theme_special_key_text_color, + liquidGlassEnable = appPreference.liquid_glass_preference, + customBorderEnable = appPreference.custom_theme_border_enable, + customBorderColor = appPreference.custom_theme_border_color, + liquidGlassKeyAlphaEnable = appPreference.liquid_glass_key_alpha, + borderWidth = appPreference.custom_theme_border_width + ) + flickView.setAngleAndRange( + appPreference.getCircularFlickRanges(), + appPreference.circular_flickWindow_scale + ) + flickView.setCircularFlickOptions(directionCount = appPreference.circularFlickDirectionCount) + flickView.applyKeySizing( + keyWidthScalePercent = appPreference.flick_key_width_scale_percent ?: 160, + keyHeightScalePercent = appPreference.flick_key_height_scale_percent ?: 160, + iconScalePercent = appPreference.flick_key_icon_scale_percent ?: 80, + textSizeSp = appPreference.flick_key_text_size_sp ?: 16.0f, + specialKeyTextSizeSp = appPreference.flick_special_key_text_size_sp ?: 16.0f + ) + flickView.applyPopupViewStyleSet( + FlickPopupViewStyleSet( + directional = PopupViewStyle( + sizeScalePercent = appPreference.flick_directional_popup_size_scale_percent ?: 100, + textSizeSp = appPreference.flick_directional_popup_text_size_sp ?: 28.0f + ), + cross = PopupViewStyle( + sizeScalePercent = appPreference.flick_cross_popup_size_scale_percent ?: 100, + textSizeSp = appPreference.flick_cross_popup_text_size_sp ?: 18.0f + ), + standard = PopupViewStyle( + sizeScalePercent = appPreference.flick_standard_popup_size_scale_percent ?: 100, + textSizeSp = appPreference.flick_standard_popup_text_size_sp ?: 19.0f + ), + tfbi = PopupViewStyle( + sizeScalePercent = appPreference.flick_tfbi_popup_size_scale_percent ?: 100, + textSizeSp = appPreference.flick_tfbi_popup_text_size_sp ?: 20.0f + ) + ) + ) + flickView.setFlickSensitivityValue(appPreference.flick_sensitivity_preference ?: 100) + flickView.setLongPressTimeout((appPreference.long_press_timeout_preference ?: 300).toLong()) + flickView.setFlickGuideEnabled(appPreference.flick_keymap_guide_layout ?: false) + flickView.setFlickGuideTextSizeSp( + (appPreference.flick_guide_text_size_sp_preference ?: 9).coerceIn(6, 16).toFloat() + ) + flickView.setFlickGuideMaxCodePoints( + (appPreference.flick_guide_max_characters_preference ?: 1).coerceIn(1, 4) + ) + flickView.setOnKeyboardActionListener(object : FlickKeyboardView.OnKeyboardActionListener { + override fun onPress(action: KeyAction) = Unit + override fun onAction(action: KeyAction, isFlick: Boolean) = Unit + override fun onActionLongPress(action: KeyAction) = Unit + override fun onActionUpAfterLongPress(action: KeyAction) = Unit + override fun onFlickDirectionChanged(direction: FlickDirection) = Unit + override fun onFlickActionLongPress(action: KeyAction) = Unit + override fun onFlickActionUpAfterLongPress(action: KeyAction, isFlick: Boolean) = Unit + override fun onLongPressActionCanceled(action: KeyAction) = Unit + }) + flickView.isClickable = false + flickView.isFocusable = false +} + +private suspend fun loadPreviewCustomKeyboardLayout( + appPreference: AppPreference, + keyboardRepository: KeyboardRepository +): PreviewCustomKeyboardLayout? { + val layouts = keyboardRepository.getLayoutsNotFlowEnsuringStableIds() + val selection = resolveInitialCustomKeyboardSelection( + layouts = layouts, + rememberLast = appPreference.remember_last_custom_keyboard_preference ?: false, + savedStableId = appPreference.last_used_custom_keyboard_stable_id + ) ?: return null + val layoutMeta = layouts.getOrNull(selection.index) ?: return null + val dbLayout = runCatching { keyboardRepository.getFullLayout(layoutMeta.layoutId).first() } + .getOrElse { + Timber.w( + it, + "loadPreviewCustomKeyboardLayout: layout disappeared id=%d stableId=%s", + layoutMeta.layoutId, + layoutMeta.stableId + ) + return null + } + val convertedLayout = keyboardRepository.convertLayout(dbLayout) + val persistenceKey = layoutMeta.stableId.takeIf { it.isNotBlank() } + ?: layoutMeta.layoutId.toString() + return PreviewCustomKeyboardLayout( + layout = convertedLayout, + isRomaji = if (appPreference.remember_custom_keyboard_input_mode_preference == true) { + appPreference.getCustomKeyboardLastRomajiMode(persistenceKey) ?: convertedLayout.isRomaji + } else { + convertedLayout.isRomaji + }, + isDirectMode = if (appPreference.remember_custom_keyboard_input_mode_preference == true) { + appPreference.getCustomKeyboardLastDirectMode(persistenceKey) ?: convertedLayout.isDirectMode + } else { + convertedLayout.isDirectMode + } + ) +} + +private fun createPreviewSumireKeyboardLayout( + context: Context, + appPreference: AppPreference, + inputMode: KeyboardInputMode, + layoutType: String, + actionOverrides: List, + placementOverrides: List +): KeyboardLayout { + val dynamicStates = mapOf( + "enter_key" to 0, + "dakuten_toggle_key" to 0, + "katakana_toggle_key" to 0, + "space_convert_key" to 0 + ) + val baseLayout = KeyboardDefaultLayouts.createFinalLayout( + mode = inputMode, + dynamicKeyStates = dynamicStates, + inputLayoutType = layoutType, + inputStyle = appPreference.sumire_keyboard_style, + deleteKeyFlickSettings = currentDeleteKeyFlickSettings(appPreference) + ) + val circularLayout = CircularSlotActionApplier.apply( + layout = baseLayout, + mode = inputMode, + settings = appPreference.getCircularSlotActionSettings() + ) + val displayLayout = SumireSpecialKeyActionDisplayOverrideApplier.apply( + layout = circularLayout, + layoutType = layoutType, + inputMode = inputMode.name, + overrides = actionOverrides, + displayMetadata = sumireSpecialKeyActionDisplayMetadata(context) + ) + return SumireSpecialKeyPlacementOverrideApplier.apply( + layout = displayLayout, + layoutType = layoutType, + inputMode = inputMode.name, + overrides = placementOverrides + ) +} + +private fun applyDeleteKeyFlickPreferences( + appPreference: AppPreference, + layout: KeyboardLayout +): KeyboardLayout { + return KeyboardDefaultLayouts.applyDeleteKeyFlickSettings( + layout = layout, + deleteKeyFlickSettings = currentDeleteKeyFlickSettings(appPreference) + ) +} + +private fun currentDeleteKeyFlickSettings( + appPreference: AppPreference +): KeyboardDefaultLayouts.DeleteKeyFlickSettings { + return KeyboardDefaultLayouts.DeleteKeyFlickSettings( + left = appPreference.delete_key_left_flick_preference, + up = appPreference.delete_key_up_flick_preference, + down = appPreference.delete_key_down_flick_preference + ) +} + +private fun syncCustomKeyboardToggleKeyIcons( + flickView: FlickKeyboardView, + isDirectMode: Boolean, + isRomaji: Boolean, + isShiftPressed: Boolean, + isCapLock: Boolean +) { + flickView.updateKeyIconByAction( + KeyAction.SwitchDirectMode, + if (isDirectMode) { + com.kazumaproject.core.R.drawable.language_japanese_kana_right_24px + } else { + com.kazumaproject.core.R.drawable.language_japanese_kana_left_24px + } + ) + flickView.updateKeyIconByAction( + KeyAction.SwitchRomajiEnglish, + if (isRomaji) { + com.kazumaproject.core.R.drawable.language_japanese_kana_left_bold_24px + } else { + com.kazumaproject.core.R.drawable.language_japanese_kana_right_bold_24px + } + ) + flickView.updateKeyIconByAction( + KeyAction.ShiftKey, + if (isShiftPressed) { + com.kazumaproject.core.R.drawable.shift_fill_24px + } else { + com.kazumaproject.core.R.drawable.shift_24px + } + ) + flickView.updateKeyIconByAction( + KeyAction.CapLockKey, + if (isCapLock) { + com.kazumaproject.core.R.drawable.caps_lock + } else { + com.kazumaproject.core.R.drawable.caps_lock_outline + } + ) +} + +private fun sumireSpecialKeyActionDisplayMetadata( + context: Context +): List { + return KeyActionMapper.getDisplayActions(context).map { + SumireSpecialKeyActionDisplayMetadata( + action = it.action, + displayName = it.displayName, + iconResId = it.iconResId + ) + } +} + +private fun currentNightMode(context: Context): Int = + context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + +private fun Context.dpToPx(value: Int): Int = + (value * resources.displayMetrics.density).roundToInt() diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_setting/CandidateViewHeightSettingFragment.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_setting/CandidateViewHeightSettingFragment.kt index 461b5b23..078a5164 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_setting/CandidateViewHeightSettingFragment.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_setting/CandidateViewHeightSettingFragment.kt @@ -1,28 +1,45 @@ package com.kazumaproject.markdownhelperkeyboard.setting_activity.ui.candidate_view_height_setting import android.annotation.SuppressLint +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.drawable.GradientDrawable import android.os.Bundle +import android.view.Gravity import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.LinearLayout import android.widget.SeekBar -import androidx.core.view.MenuHost -import androidx.core.view.MenuProvider +import androidx.annotation.AttrRes +import androidx.appcompat.R as AppCompatR +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.AppCompatImageButton +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.view.isInvisible +import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.R as MaterialR +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import com.google.android.material.textview.MaterialTextView import com.kazumaproject.markdownhelperkeyboard.R -import com.kazumaproject.markdownhelperkeyboard.converter.candidate.Candidate import com.kazumaproject.markdownhelperkeyboard.databinding.FragmentCandidateViewHeightSettingBinding -import com.kazumaproject.markdownhelperkeyboard.ime_service.adapters.GridSpacingItemDecoration +import com.kazumaproject.markdownhelperkeyboard.ime_service.CandidateStripPresentationPolicy +import com.kazumaproject.markdownhelperkeyboard.ime_service.CandidateStripPresentationState +import com.kazumaproject.markdownhelperkeyboard.ime_service.state.CandidateTab +import com.kazumaproject.markdownhelperkeyboard.repository.KeyboardRepository import com.kazumaproject.markdownhelperkeyboard.setting_activity.AppPreference +import com.kazumaproject.markdownhelperkeyboard.short_cut.ShortcutType +import com.kazumaproject.markdownhelperkeyboard.sumire_special_key.SumireSpecialKeyRepository import dagger.hilt.android.AndroidEntryPoint import timber.log.Timber +import java.util.Locale import javax.inject.Inject import kotlin.math.roundToInt @@ -32,41 +49,47 @@ class CandidateViewHeightSettingFragment : Fragment() { @Inject lateinit var appPreference: AppPreference + @Inject + lateinit var keyboardRepository: KeyboardRepository + + @Inject + lateinit var sumireSpecialKeyRepository: SumireSpecialKeyRepository + private lateinit var suggestionAdapter: SuggestionAdapter2 - private lateinit var candidateList: List private var _binding: FragmentCandidateViewHeightSettingBinding? = null private val binding get() = _binding!! - // State to track if the candidate list is visible. Set to false for empty by default. - private var isCandidateListVisible = false + private var isCandidateListVisible = true private var isSyncingHeightControls = false + private var isSyncingLetterSizeControls = false + private var isSyncingColumnControls = false + private var isSyncingDefaultHeightControls = false + private var previousBottomNavigationVisibility: Int? = null + private var previousNavHostBottomToTop = ConstraintLayout.LayoutParams.UNSET + private var previousNavHostBottomToBottom = ConstraintLayout.LayoutParams.UNSET + private var previousNavHostBottomMargin = 0 + private var hasPreviousNavHostBottomConstraint = false private val minHeightDp = 30 private val maxHeightDp = 300 - private val defaultHeightDp = 110 + private val minCandidateTextSize = 10f + private val maxCandidateTextSize = 40f + private val defaultCandidateTextSize = 14.0f + + private val previewCandidates = createCandidateHeightPreviewCandidates() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) suggestionAdapter = SuggestionAdapter2() - candidateList = (1..16).map { index -> - Candidate( - string = "候補 $index", - type = (index % 4).toByte(), - length = "候補 $index".length.toUByte(), - score = 100 - index, - leftId = (index * 10).toShort(), - rightId = (index * 10 + 1).toShort() - ) - } } override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, + inflater: LayoutInflater, + container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding = FragmentCandidateViewHeightSettingBinding.inflate(inflater, container, false) - setupMenu() return binding.root } @@ -74,104 +97,268 @@ class CandidateViewHeightSettingFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + hideBottomNavigationForPreview() + appPreference.migrateCandidateHeightPerColumnPreferencesIfNeeded() + appPreference.syncActiveCandidateVisibleHeightToImePreference(isLandscape = false) + + setupMenu() + setupAdapter() + setupInspectorBottomSheet() + setupInspectorTabs() + setupColumnControls() setupResizeHandle() + setupHeightSeekBar() + setupHeightEditText() + setupCandidateLetterSizeSeekBar() + setupCandidateLetterSizeEditText() + setupDefaultHeightControls() + setupKeyboardPreview() setSuggestionView() - suggestionAdapter.apply { - setUndoEnabled(false) - setPasteEnabled(false) - - onListUpdated = { - applyCurrentDimensions() - } - } - - binding.toggleCandidateListButton.setOnClickListener { isCandidateListVisible = !isCandidateListVisible updateCandidateListAndHeight() } - binding.candidateHeightSettingTenkeyPreview.apply { - setOnTouchListener { _, _ -> - false - } - } - setupHeightSeekBar() - setupHeightEditText() - - // Set initial state + applyCandidateTextSize(appPreference.candidate_letter_size ?: defaultCandidateTextSize, persist = false) updateCandidateListAndHeight() applyHeightDp(selectedHeightDp(), persist = false) + syncDefaultHeightControls() + updateInspectorSummary() } override fun onDestroyView() { super.onDestroyView() - isCandidateListVisible = false + restoreBottomNavigationVisibility() + (activity as? AppCompatActivity)?.supportActionBar?.show() + binding.candidateHeightSettingRecyclerview.adapter = null + suggestionAdapter.release() _binding = null } - private fun setupMenu() { - val menuHost: MenuHost = requireActivity() - menuHost.addMenuProvider(object : MenuProvider { - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - menuInflater.inflate(R.menu.fragment_reset_menu, menu) - } + private fun hideBottomNavigationForPreview() { + val bottomNavigation = activity?.findViewById(R.id.nav_view) ?: return + if (previousBottomNavigationVisibility == null) { + previousBottomNavigationVisibility = bottomNavigation.visibility + } + bottomNavigation.visibility = View.GONE + extendNavHostToParentBottom() + } - override fun onMenuItemSelected(menuItem: MenuItem): Boolean { - return when (menuItem.itemId) { - R.id.action_reset -> { - resetSettings() - true - } + private fun restoreBottomNavigationVisibility() { + val visibility = previousBottomNavigationVisibility ?: return + restoreNavHostBottomConstraint() + activity?.findViewById(R.id.nav_view)?.visibility = visibility + previousBottomNavigationVisibility = null + } - android.R.id.home -> { - parentFragmentManager.popBackStack() - true - } + private fun extendNavHostToParentBottom() { + val container = activity?.findViewById(R.id.container) ?: return + val navHost = activity?.findViewById(R.id.nav_host_fragment_activity_main) ?: return + val layoutParams = navHost.layoutParams as? ConstraintLayout.LayoutParams ?: return + if (!hasPreviousNavHostBottomConstraint) { + previousNavHostBottomToTop = layoutParams.bottomToTop + previousNavHostBottomToBottom = layoutParams.bottomToBottom + previousNavHostBottomMargin = layoutParams.bottomMargin + hasPreviousNavHostBottomConstraint = true + } + ConstraintSet().apply { + clone(container) + clear(R.id.nav_host_fragment_activity_main, ConstraintSet.BOTTOM) + connect( + R.id.nav_host_fragment_activity_main, + ConstraintSet.BOTTOM, + ConstraintSet.PARENT_ID, + ConstraintSet.BOTTOM + ) + setMargin(R.id.nav_host_fragment_activity_main, ConstraintSet.BOTTOM, 0) + applyTo(container) + } + } - else -> false + private fun restoreNavHostBottomConstraint() { + if (!hasPreviousNavHostBottomConstraint) return + val container = activity?.findViewById(R.id.container) ?: return + ConstraintSet().apply { + clone(container) + clear(R.id.nav_host_fragment_activity_main, ConstraintSet.BOTTOM) + when { + previousNavHostBottomToTop != ConstraintLayout.LayoutParams.UNSET -> { + connect( + R.id.nav_host_fragment_activity_main, + ConstraintSet.BOTTOM, + previousNavHostBottomToTop, + ConstraintSet.TOP + ) + } + + previousNavHostBottomToBottom != ConstraintLayout.LayoutParams.UNSET -> { + connect( + R.id.nav_host_fragment_activity_main, + ConstraintSet.BOTTOM, + previousNavHostBottomToBottom, + ConstraintSet.BOTTOM + ) } } - }, viewLifecycleOwner, Lifecycle.State.RESUMED) + setMargin( + R.id.nav_host_fragment_activity_main, + ConstraintSet.BOTTOM, + previousNavHostBottomMargin + ) + applyTo(container) + } + hasPreviousNavHostBottomConstraint = false } - private fun resetSettings() { - // Reset height for when candidates are visible - if (!isCandidateListVisible) { - appPreference.candidate_view_height_dp = defaultHeightDp - } else { - when (appPreference.candidate_column_preference) { - "1" -> { - appPreference.candidate_view_height_dp = defaultHeightDp + private fun setupMenu() { + (activity as? AppCompatActivity)?.supportActionBar?.hide() + binding.toolbar.setNavigationIcon(AppCompatR.drawable.abc_ic_ab_back_material) + binding.toolbar.setNavigationOnClickListener { + parentFragmentManager.popBackStack() + } + binding.toolbar.inflateMenu(R.menu.fragment_reset_menu) + binding.toolbar.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.action_candidate_default_height -> { + openDefaultHeightInspector() + true } - "2" -> { - appPreference.candidate_view_height_dp = 165 + R.id.action_reset -> { + resetSettings() + true } - "3" -> { - appPreference.candidate_view_height_dp = 230 - } + else -> false + } + } + } + + private fun setupInspectorBottomSheet() { + binding.showInspectorButton.setOnClickListener { + showInspectorPanel() + } + binding.closeInspectorButton.setOnClickListener { + hideInspectorPanel() + } + showInspectorPanel() + } + + private fun setupInspectorTabs() { + binding.candidateInspectorTabGroup.check(R.id.candidate_tab_height_button) + binding.candidateInspectorTabGroup.addOnButtonCheckedListener { _, checkedId, isChecked -> + if (!isChecked) return@addOnButtonCheckedListener + showInspectorTab(checkedId) + } + showInspectorTab(R.id.candidate_tab_height_button) + } + + private fun openDefaultHeightInspector() { + binding.candidateInspectorTabGroup.check(R.id.candidate_tab_default_button) + syncDefaultHeightControls() + showInspectorPanel() + } + + private fun showInspectorPanel() { + binding.inspectorBottomSheet.isVisible = true + binding.showInspectorButton.isVisible = false + } + + private fun hideInspectorPanel() { + binding.inspectorBottomSheet.isVisible = false + binding.showInspectorButton.isVisible = true + } + + private fun showInspectorTab(checkedId: Int) { + binding.heightControlsContainer.isVisible = checkedId == R.id.candidate_tab_height_button + binding.textControlsContainer.isVisible = checkedId == R.id.candidate_tab_text_button + binding.defaultControlsContainer.isVisible = + checkedId == R.id.candidate_tab_default_button + if (checkedId == R.id.candidate_tab_default_button) { + syncDefaultHeightControls() + } + } + + private fun setupAdapter() { + suggestionAdapter.apply { + setUndoEnabled(false) + setPasteEnabled(false) + onListUpdated = { + applyCurrentDimensions() } } - appPreference.candidate_view_empty_height_dp = defaultHeightDp + applyCandidateAdapterPresentation() + binding.candidateHeightSettingRecyclerview.apply { + itemAnimator = null + isFocusable = false + } + binding.candidateHeightSettingRecyclerview.adapter = suggestionAdapter + } + + private fun applyCandidateAdapterPresentation() { + suggestionAdapter.setShowCandidateYomiForLiveConversion( + (appPreference.live_conversion_preference ?: false) && + (appPreference.live_conversion_candidate_yomi_preference ?: false) + ) + if (appPreference.theme_mode == "custom") { + suggestionAdapter.setCandidateTextColor(appPreference.custom_theme_candidate_text_color) + suggestionAdapter.setCandidateItemColors( + backgroundColor = appPreference.custom_theme_candidate_item_bg_color, + pressedColor = appPreference.custom_theme_candidate_item_pressed_bg_color + ) + suggestionAdapter.setCandidateEmptyPopupColors( + backgroundColor = appPreference.custom_theme_candidate_empty_popup_bg_color, + textColor = appPreference.custom_theme_candidate_empty_popup_text_color + ) + } else { + suggestionAdapter.clearCandidateEmptyPopupColors() + } + } + + private fun setupColumnControls() { + syncColumnControls() + binding.candidateColumnToggleGroup.addOnButtonCheckedListener { _, checkedId, isChecked -> + if (!isChecked || isSyncingColumnControls) return@addOnButtonCheckedListener + val column = columnForButtonId(checkedId) ?: return@addOnButtonCheckedListener + appPreference.setCandidateColumnAndSyncHeight(isLandscape = false, column = column) + setSuggestionView() + syncColumnControls() + applyHeightDp(selectedHeightDp(), persist = false) + updateInspectorSummary() + } + } + + private fun resetSettings() { + appPreference.resetCandidateVisibleHeightsToUserDefaults(isLandscape = false) + appPreference.syncActiveCandidateVisibleHeightToImePreference(isLandscape = false) + appPreference.candidate_letter_size = defaultCandidateTextSize + applyCandidateTextSize(defaultCandidateTextSize, persist = false) + syncColumnControls() + updateCandidateListAndHeight() applyHeightDp(selectedHeightDp(), persist = false) + updateInspectorSummary() } private fun updateCandidateListAndHeight() { if (isCandidateListVisible) { - suggestionAdapter.suggestions = candidateList - binding.toggleCandidateListButton.text = "入力時" + suggestionAdapter.suggestions = previewCandidates + binding.toggleCandidateListButton.text = getString(R.string.candidate_preview_input_mode) } else { suggestionAdapter.suggestions = emptyList() - binding.toggleCandidateListButton.text = "未入力時" + binding.toggleCandidateListButton.text = getString(R.string.candidate_preview_empty_mode) } + binding.candidatePreviewVisibilityButton.isVisible = isCandidateListVisible + updateCandidateTabPreview() + updateShortcutToolbarPreview() applyHeightDp(selectedHeightDp(), persist = false) + updateInspectorSummary() } private fun setSuggestionView() { - when (val columnNum = appPreference.candidate_column_preference) { + val columnNum = appPreference.getCandidateColumn(isLandscape = false) + clearItemDecorations(binding.candidateHeightSettingRecyclerview) + when (columnNum) { "1" -> { binding.candidateHeightSettingRecyclerview.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false) @@ -179,45 +366,62 @@ class CandidateViewHeightSettingFragment : Fragment() { "2", "3" -> { val spanCount = columnNum.toInt() - val gridLayoutManager = GridLayoutManager( - requireContext(), spanCount, GridLayoutManager.HORIZONTAL, false - ) + val gridLayoutManager = + GridLayoutManager( + requireContext(), + spanCount, + GridLayoutManager.HORIZONTAL, + false + ).apply { + spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + return if (suggestionAdapter.isFullSpanItem(position)) { + spanCount + } else { + 1 + } + } + } + } + binding.candidateHeightSettingRecyclerview.layoutManager = gridLayoutManager val spacingInPixels = resources.getDimensionPixelSize(com.kazumaproject.core.R.dimen.grid_spacing) - - binding.candidateHeightSettingRecyclerview.layoutManager = - gridLayoutManager binding.candidateHeightSettingRecyclerview.addItemDecoration( - GridSpacingItemDecoration( - spanCount, spacingInPixels, true + CandidateHeightPreviewGridSpacingDecoration( + spanCount = spanCount, + spacing = spacingInPixels, + includeEdge = true ) ) } } - binding.candidateHeightSettingRecyclerview.apply { - adapter = suggestionAdapter - } + binding.candidateHeightSettingRecyclerview.adapter = suggestionAdapter } - /** - * 保存された高さ設定をビューに適用する - */ private fun applyCurrentDimensions() { applyHeightDp(selectedHeightDp(), persist = false) } private fun selectedHeightDp(): Int { return if (isCandidateListVisible) { - appPreference.candidate_view_height_dp ?: defaultHeightDp + appPreference.getCandidateVisibleHeightDp( + isLandscape = false, + column = appPreference.getCandidateColumn(isLandscape = false) + ) } else { - appPreference.candidate_view_empty_height_dp ?: defaultHeightDp + appPreference.candidate_view_empty_height_dp ?: 110 } } private fun saveSelectedHeightDp(heightDp: Int) { val clamped = heightDp.coerceIn(minHeightDp, maxHeightDp) if (isCandidateListVisible) { - appPreference.candidate_view_height_dp = clamped + appPreference.setCandidateVisibleHeightDp( + isLandscape = false, + column = appPreference.getCandidateColumn(isLandscape = false), + heightDp = clamped + ) + appPreference.syncActiveCandidateVisibleHeightToImePreference(isLandscape = false) } else { appPreference.candidate_view_empty_height_dp = clamped } @@ -225,32 +429,53 @@ class CandidateViewHeightSettingFragment : Fragment() { private fun applyHeightDp(heightDp: Int, persist: Boolean) { val clamped = heightDp.coerceIn(minHeightDp, maxHeightDp) - val heightPx = (clamped * resources.displayMetrics.density).toInt() + val heightPx = clamped.dpToPx() updatePreviewHeightPx(heightPx) if (persist) { saveSelectedHeightDp(clamped) } syncHeightControls(clamped) + updateInspectorSummary() } private fun updatePreviewHeightPx(candidateHeightPx: Int) { - binding.candidateHeightSettingRecyclerview.layoutParams = - binding.candidateHeightSettingRecyclerview.layoutParams.apply { + val presentation = resolveCandidateHeightPreviewPresentation() + val keyboardHeightPx = keyboardPreviewHeightPx() + val independentToolbarHeightPx = presentation.independentShortcutToolbarHeightPx + binding.keyboardPreviewContainer.layoutParams = + (binding.keyboardPreviewContainer.layoutParams as FrameLayout.LayoutParams).apply { + gravity = Gravity.BOTTOM + height = keyboardHeightPx + bottomMargin = 0 + } + binding.candidatePreviewFrame.layoutParams = + (binding.candidatePreviewFrame.layoutParams as FrameLayout.LayoutParams).apply { + gravity = Gravity.BOTTOM height = candidateHeightPx + bottomMargin = keyboardHeightPx + } + binding.independentShortcutToolbarPreviewContainer.layoutParams = + (binding.independentShortcutToolbarPreviewContainer.layoutParams as FrameLayout.LayoutParams).apply { + gravity = Gravity.BOTTOM + height = independentToolbarHeightPx.coerceAtLeast(36.dpToPx()) + bottomMargin = keyboardHeightPx + candidateHeightPx + } + binding.candidateTabPreviewContainer.layoutParams = + (binding.candidateTabPreviewContainer.layoutParams as FrameLayout.LayoutParams).apply { + gravity = Gravity.TOP + height = presentation.candidateTabOffsetPx.coerceAtLeast(36.dpToPx()) } binding.candidateHeightSettingContent.layoutParams = binding.candidateHeightSettingContent.layoutParams.apply { - height = candidateHeightPx + keyboardPreviewHeightPx() + height = presentation.candidateTabOffsetPx + + candidateHeightPx + + independentToolbarHeightPx + + keyboardHeightPx } } private fun keyboardPreviewHeightPx(): Int { - val previewHeight = binding.candidateHeightSettingTenkeyPreview.layoutParams.height - return if (previewHeight > 0) { - previewHeight - } else { - (280 * resources.displayMetrics.density).toInt() - } + return candidateKeyboardPreviewHeightPx(binding.keyboardPreviewContainer) } private fun syncHeightControls(heightDp: Int) { @@ -282,8 +507,7 @@ class CandidateViewHeightSettingFragment : Fragment() { fromUser: Boolean ) { if (!fromUser || isSyncingHeightControls) return - val heightDp = minHeightDp + progress - applyHeightDp(heightDp, persist = true) + applyHeightDp(minHeightDp + progress, persist = true) } override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit @@ -307,20 +531,206 @@ class CandidateViewHeightSettingFragment : Fragment() { private fun applyHeightFromEditText() { if (isSyncingHeightControls) return - val raw = binding.candidateHeightEditText.text?.toString()?.trim() - val value = raw?.toIntOrNull() + val value = binding.candidateHeightEditText.text?.toString()?.trim()?.toIntOrNull() if (value == null) { binding.candidateHeightInputLayout.error = getString(R.string.candidate_height_invalid_value) return } - val clamped = value.coerceIn(minHeightDp, maxHeightDp) - applyHeightDp(clamped, persist = true) + applyHeightDp(value.coerceIn(minHeightDp, maxHeightDp), persist = true) + } + + private fun setupCandidateLetterSizeSeekBar() { + binding.candidateLetterSizeSeekbar.max = + ((maxCandidateTextSize - minCandidateTextSize) * 10).roundToInt() + binding.candidateLetterSizeSeekbar.setOnSeekBarChangeListener( + object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged( + seekBar: SeekBar?, + progress: Int, + fromUser: Boolean + ) { + if (!fromUser || isSyncingLetterSizeControls) return + val newSize = minCandidateTextSize + progress / 10f + applyCandidateTextSize(newSize, persist = true) + } + + override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit + + override fun onStopTrackingTouch(seekBar: SeekBar?) = Unit + } + ) + } + + private fun setupCandidateLetterSizeEditText() { + binding.candidateLetterSizeEditText.setOnEditorActionListener { _, _, _ -> + applyCandidateLetterSizeFromEditText() + false + } + binding.candidateLetterSizeEditText.setOnFocusChangeListener { _, hasFocus -> + if (!hasFocus) { + applyCandidateLetterSizeFromEditText() + } + } + } + + private fun applyCandidateLetterSizeFromEditText() { + if (isSyncingLetterSizeControls) return + val value = binding.candidateLetterSizeEditText.text?.toString()?.trim()?.toFloatOrNull() + if (value == null) { + binding.candidateLetterSizeInputLayout.error = + getString(R.string.candidate_letter_size_invalid_value) + return + } + applyCandidateTextSize(value.coerceIn(minCandidateTextSize, maxCandidateTextSize), persist = true) + } + + private fun applyCandidateTextSize(size: Float, persist: Boolean) { + val clamped = ((size.coerceIn(minCandidateTextSize, maxCandidateTextSize) * 10).roundToInt() / 10f) + suggestionAdapter.setCandidateTextSize(clamped) + if (persist) { + appPreference.candidate_letter_size = clamped + } + syncCandidateLetterSizeControls(clamped) + updateInspectorSummary() + } + + private fun syncCandidateLetterSizeControls(size: Float) { + if (isSyncingLetterSizeControls) return + isSyncingLetterSizeControls = true + try { + val clamped = size.coerceIn(minCandidateTextSize, maxCandidateTextSize) + binding.candidateLetterSizeSeekbar.progress = + ((clamped - minCandidateTextSize) * 10).roundToInt() + val text = String.format(Locale.US, "%.1f", clamped) + if (binding.candidateLetterSizeEditText.text?.toString() != text) { + binding.candidateLetterSizeEditText.setText(text) + binding.candidateLetterSizeEditText.setSelection(text.length) + } + binding.candidateLetterSizeInputLayout.error = null + } finally { + isSyncingLetterSizeControls = false + } + } + + private fun setupDefaultHeightControls() { + binding.saveDefaultsButton.setOnClickListener { + saveDefaultHeightsFromInputs() + } + binding.useCurrentDefaultsButton.setOnClickListener { + appPreference.copyCandidateVisibleHeightsToUserDefaults(isLandscape = false) + syncDefaultHeightControls() + } + binding.restoreFactoryDefaultsButton.setOnClickListener { + appPreference.resetCandidateDefaultVisibleHeightsToFactoryDefaults(isLandscape = false) + syncDefaultHeightControls() + } + listOf( + binding.defaultHeightOneEditText, + binding.defaultHeightTwoEditText, + binding.defaultHeightThreeEditText + ).forEach { editText -> + editText.setOnEditorActionListener { _, _, _ -> + saveDefaultHeightsFromInputs() + false + } + } + } + + private fun saveDefaultHeightsFromInputs(): Boolean { + if (isSyncingDefaultHeightControls) return false + val one = readDefaultHeightInput( + binding.defaultHeightOneInputLayout, + binding.defaultHeightOneEditText + ) ?: return false + val two = readDefaultHeightInput( + binding.defaultHeightTwoInputLayout, + binding.defaultHeightTwoEditText + ) ?: return false + val three = readDefaultHeightInput( + binding.defaultHeightThreeInputLayout, + binding.defaultHeightThreeEditText + ) ?: return false + + appPreference.setCandidateDefaultVisibleHeightDp( + isLandscape = false, + column = "1", + heightDp = one + ) + appPreference.setCandidateDefaultVisibleHeightDp( + isLandscape = false, + column = "2", + heightDp = two + ) + appPreference.setCandidateDefaultVisibleHeightDp( + isLandscape = false, + column = "3", + heightDp = three + ) + syncDefaultHeightControls() + return true + } + + private fun readDefaultHeightInput( + inputLayout: TextInputLayout, + editText: TextInputEditText + ): Int? { + val value = editText.text?.toString()?.trim()?.toIntOrNull() + if (value == null) { + inputLayout.error = getString(R.string.candidate_height_invalid_value) + return null + } + inputLayout.error = null + return value.coerceIn(minHeightDp, maxHeightDp) + } + + private fun syncDefaultHeightControls() { + if (isSyncingDefaultHeightControls) return + isSyncingDefaultHeightControls = true + try { + setDefaultHeightText( + binding.defaultHeightOneInputLayout, + binding.defaultHeightOneEditText, + appPreference.getCandidateDefaultVisibleHeightDp(isLandscape = false, column = "1") + ) + setDefaultHeightText( + binding.defaultHeightTwoInputLayout, + binding.defaultHeightTwoEditText, + appPreference.getCandidateDefaultVisibleHeightDp(isLandscape = false, column = "2") + ) + setDefaultHeightText( + binding.defaultHeightThreeInputLayout, + binding.defaultHeightThreeEditText, + appPreference.getCandidateDefaultVisibleHeightDp(isLandscape = false, column = "3") + ) + } finally { + isSyncingDefaultHeightControls = false + } + } + + private fun setDefaultHeightText( + inputLayout: TextInputLayout, + editText: TextInputEditText, + heightDp: Int + ) { + val text = heightDp.coerceIn(minHeightDp, maxHeightDp).toString() + if (editText.text?.toString() != text) { + editText.setText(text) + editText.setSelection(text.length) + } + inputLayout.error = null + } + + private fun updateInspectorSummary(heightDp: Int = selectedHeightDp()) { + val textSize = appPreference.candidate_letter_size ?: defaultCandidateTextSize + binding.inspectorSummaryText.text = getString( + R.string.candidate_height_sheet_summary_format, + appPreference.getCandidateColumn(isLandscape = false), + heightDp.coerceIn(minHeightDp, maxHeightDp).toString(), + String.format(Locale.US, "%.1f", textSize) + ) } - /** - * 高さ変更ハンドルのタッチリスナーを設定する - */ @SuppressLint("ClickableViewAccessibility") private fun setupResizeHandle() { var initialY = 0f @@ -333,21 +743,32 @@ class CandidateViewHeightSettingFragment : Fragment() { binding.handleTop.setOnTouchListener { _, event -> when (event.action) { MotionEvent.ACTION_DOWN -> { + binding.handleTop.parent?.requestDisallowInterceptTouchEvent(true) + binding.candidateControlsScroll.requestDisallowInterceptTouchEvent(true) initialY = event.rawY - initialHeight = binding.candidateHeightSettingRecyclerview.height + initialHeight = binding.candidatePreviewFrame.height true } MotionEvent.ACTION_MOVE -> { + binding.handleTop.parent?.requestDisallowInterceptTouchEvent(true) + binding.candidateControlsScroll.requestDisallowInterceptTouchEvent(true) val deltaY = event.rawY - initialY val newHeight = (initialHeight - deltaY).coerceIn(minHeightPx, maxHeightPx) - updatePreviewHeightPx(newHeight.toInt()) - binding.candidateHeightSettingRecyclerview.requestLayout() + val currentHeightDp = + (newHeight / density).roundToInt().coerceIn(minHeightDp, maxHeightDp) + updatePreviewHeightPx(currentHeightDp.dpToPx()) + syncHeightControls(currentHeightDp) + updateInspectorSummary(currentHeightDp) + binding.candidatePreviewFrame.requestLayout() binding.candidateHeightSettingContent.requestLayout() true } - MotionEvent.ACTION_UP -> { + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> { + binding.handleTop.parent?.requestDisallowInterceptTouchEvent(false) + binding.candidateControlsScroll.requestDisallowInterceptTouchEvent(false) val finalHeightDp = saveHeightPreference() applyHeightDp(finalHeightDp, persist = false) true @@ -358,24 +779,231 @@ class CandidateViewHeightSettingFragment : Fragment() { } } - /** - * 現在のビューの高さを SharedPreferences に保存する - */ private fun saveHeightPreference(): Int { val density = resources.displayMetrics.density - val heightPx = binding.candidateHeightSettingRecyclerview.layoutParams.height + val heightPx = binding.candidatePreviewFrame.layoutParams.height .takeIf { it > 0 } - ?: binding.candidateHeightSettingRecyclerview.height + ?: binding.candidatePreviewFrame.height val finalHeightDp = (heightPx / density).roundToInt().coerceIn(minHeightDp, maxHeightDp) saveSelectedHeightDp(finalHeightDp) - if (isCandidateListVisible) { - Timber.d("saveHeightPreference (with candidates): $finalHeightDp dp") - } else { - Timber.d("saveHeightPreference (empty): $finalHeightDp dp") - } + Timber.d( + "saveHeightPreference portrait (%s): %d dp", + if (isCandidateListVisible) "with candidates" else "empty", + finalHeightDp + ) return finalHeightDp } + + private fun syncColumnControls() { + if (isSyncingColumnControls) return + isSyncingColumnControls = true + try { + binding.candidateColumnToggleGroup.check( + when (appPreference.getCandidateColumn(isLandscape = false)) { + "2" -> R.id.candidate_column_two_button + "3" -> R.id.candidate_column_three_button + else -> R.id.candidate_column_one_button + } + ) + } finally { + isSyncingColumnControls = false + } + } + + private fun columnForButtonId(id: Int): String? = + when (id) { + R.id.candidate_column_one_button -> "1" + R.id.candidate_column_two_button -> "2" + R.id.candidate_column_three_button -> "3" + else -> null + } + + private fun updateCandidateTabPreview() { + val presentation = resolveCandidateHeightPreviewPresentation() + binding.candidateTabPreviewContainer.isVisible = presentation.showCandidateTab + if (!presentation.showCandidateTab) return + + binding.candidateTabPreviewContainer.removeAllViews() + val tabs = runCatching { appPreference.candidate_tab_order } + .getOrDefault(listOf(CandidateTab.PREDICTION, CandidateTab.CONVERSION, CandidateTab.EISUKANA)) + tabs.forEachIndexed { index, tab -> + binding.candidateTabPreviewContainer.addView( + previewTabView(label = tab.previewLabel(), selected = index == 0), + LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f) + ) + } + } + + private fun updateShortcutToolbarPreview() { + val presentation = resolveCandidateHeightPreviewPresentation() + when { + presentation.showIndependentShortcutToolbar -> { + binding.independentShortcutToolbarPreviewContainer.isVisible = true + populateShortcutToolbarPreview(binding.independentShortcutToolbarPreviewContainer) + } + + presentation.reserveIndependentShortcutToolbarSpace -> { + binding.independentShortcutToolbarPreviewContainer.isInvisible = true + } + + else -> { + binding.independentShortcutToolbarPreviewContainer.isVisible = false + } + } + suggestionAdapter.setShortcutItems(previewShortcutItems()) + suggestionAdapter.setIntegratedShortcutVisibility(presentation.showIntegratedShortcut) + } + + private data class CandidateHeightPreviewPresentation( + val showCandidateTab: Boolean, + val showIndependentShortcutToolbar: Boolean, + val reserveIndependentShortcutToolbarSpace: Boolean, + val showIntegratedShortcut: Boolean, + val candidateTabOffsetPx: Int, + val independentShortcutToolbarHeightPx: Int + ) + + private fun resolveCandidateHeightPreviewPresentation(): CandidateHeightPreviewPresentation { + val inputStringEmpty = !isCandidateListVisible + val tailEmpty = true + val clipboardPreviewShown = false + val selectedTextGemmaActionsShown = false + val suggestionsEmpty = !isCandidateListVisible + val customLayoutPickerShown = false + val presentation = CandidateStripPresentationPolicy.resolve( + CandidateStripPresentationState( + candidateTabVisible = appPreference.candidate_tab_preference, + candidatesShown = isCandidateListVisible, + resetCandidateTabSelection = false, + shortcutToolbarVisible = appPreference.shortcut_toolbar_visibility_preference, + shortcutToolbarIntegratedInSuggestion = + appPreference.shortcut_toolbar_integrated_in_suggestion_preference, + inputStringEmpty = inputStringEmpty, + tailEmpty = tailEmpty, + clipboardPreviewShown = clipboardPreviewShown, + selectedTextGemmaActionsShown = selectedTextGemmaActionsShown, + suggestionsEmpty = suggestionsEmpty, + customLayoutPickerShown = customLayoutPickerShown, + symbolKeyboardShown = false, + shortcutToolbarHiddenForCandidates = false + ) + ) + val independentHeightPx = + if ( + presentation.showIndependentShortcutToolbar || + presentation.reserveIndependentShortcutToolbarSpace + ) { + 36.dpToPx() + } else { + 0 + } + return CandidateHeightPreviewPresentation( + showCandidateTab = presentation.showCandidateTab, + showIndependentShortcutToolbar = presentation.showIndependentShortcutToolbar, + reserveIndependentShortcutToolbarSpace = + presentation.reserveIndependentShortcutToolbarSpace, + showIntegratedShortcut = presentation.showIntegratedShortcut, + candidateTabOffsetPx = if (presentation.showCandidateTab) 36.dpToPx() else 0, + independentShortcutToolbarHeightPx = independentHeightPx + ) + } + + private fun previewShortcutItems(): List = + listOf( + ShortcutType.SETTINGS, + ShortcutType.EMOJI, + ShortcutType.TEMPLATE, + ShortcutType.KEYBOARD_PICKER, + ShortcutType.PASTE + ) + + private fun populateShortcutToolbarPreview(container: LinearLayout) { + container.removeAllViews() + previewShortcutItems().forEach { shortcut -> + val button = AppCompatImageButton(requireContext()).apply { + setImageResource(shortcut.iconResId) + imageTintList = ColorStateList.valueOf(resolveThemeColor(MaterialR.attr.colorOnSurface)) + background = null + contentDescription = shortcut.description + isClickable = false + isFocusable = false + setPadding(8.dpToPx(), 6.dpToPx(), 8.dpToPx(), 6.dpToPx()) + } + container.addView( + button, + LinearLayout.LayoutParams(42.dpToPx(), ViewGroup.LayoutParams.MATCH_PARENT) + ) + } + } + + private fun setupKeyboardPreview() { + renderCandidateKeyboardPreview( + fragment = this, + appPreference = appPreference, + keyboardRepository = keyboardRepository, + sumireSpecialKeyRepository = sumireSpecialKeyRepository, + views = CandidateKeyboardPreviewViews( + container = binding.keyboardPreviewContainer, + tenKey = binding.candidateHeightSettingTenkeyPreview, + qwerty = binding.candidateHeightSettingQwertyPreview, + flick = binding.candidateHeightSettingFlickPreview + ), + isLandscape = false, + onPreviewLayoutChanged = ::applyCurrentDimensions + ) + } + + private fun previewTabView(label: String, selected: Boolean): MaterialTextView { + return MaterialTextView(requireContext()).apply { + text = label + gravity = Gravity.CENTER + textSize = 13f + setTextColor( + resolveThemeColor( + if (selected) MaterialR.attr.colorOnPrimary else MaterialR.attr.colorOnSurface + ) + ) + background = roundedBackground( + fillColor = resolveThemeColor( + if (selected) AppCompatR.attr.colorPrimary else MaterialR.attr.colorSurfaceVariant + ), + radiusDp = 6 + ) + } + } + + private fun roundedBackground( + fillColor: Int, + strokeColor: Int? = null, + radiusDp: Int + ): GradientDrawable { + return GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + cornerRadius = radiusDp.dpToPx().toFloat() + setColor(fillColor) + strokeColor?.let { setStroke(1.dpToPx(), it) } + } + } + + private fun CandidateTab.previewLabel(): String = + when (this) { + CandidateTab.PREDICTION -> "予測" + CandidateTab.CONVERSION -> "変換" + CandidateTab.EISUKANA -> "英数カナ" + } + + private fun resolveThemeColor(@AttrRes attr: Int): Int { + val typedArray = requireContext().obtainStyledAttributes(intArrayOf(attr)) + return try { + typedArray.getColor(0, Color.TRANSPARENT) + } finally { + typedArray.recycle() + } + } + + private fun Int.dpToPx(): Int = + (this * resources.displayMetrics.density).roundToInt() } diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_setting/SuggestionAdapter2.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_setting/SuggestionAdapter2.kt index 4ab0621a..f139222f 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_setting/SuggestionAdapter2.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/candidate_view_height_setting/SuggestionAdapter2.kt @@ -1,6 +1,10 @@ package com.kazumaproject.markdownhelperkeyboard.setting_activity.ui.candidate_view_height_setting import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.PorterDuff +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.StateListDrawable import android.text.SpannableString import android.text.Spanned import android.text.style.RelativeSizeSpan @@ -10,27 +14,80 @@ import android.view.ViewGroup import android.widget.ImageView import androidx.appcompat.widget.AppCompatImageButton import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat import androidx.core.view.isVisible +import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.RecyclerView import com.google.android.material.color.DynamicColors import com.google.android.material.textview.MaterialTextView import com.kazumaproject.core.domain.extensions.isAllFullWidthNumericSymbol import com.kazumaproject.core.domain.extensions.isAllHalfWidthNumericSymbol import com.kazumaproject.core.domain.extensions.isDarkThemeOn +import com.kazumaproject.core.domain.extensions.setDrawableSolidColor import com.kazumaproject.core.domain.state.TenKeyQWERTYMode import com.kazumaproject.markdownhelperkeyboard.R import com.kazumaproject.markdownhelperkeyboard.converter.candidate.Candidate +import com.kazumaproject.markdownhelperkeyboard.converter.candidate.QWERTY_GLIDE_CANDIDATE_TYPE import com.kazumaproject.markdownhelperkeyboard.custom_keyboard.data.CustomKeyboardLayout import com.kazumaproject.markdownhelperkeyboard.gemma.GemmaTranslationManager import com.kazumaproject.markdownhelperkeyboard.ime_service.extensions.correctReading import com.kazumaproject.markdownhelperkeyboard.ime_service.extensions.debugPrintCodePoints +import com.kazumaproject.markdownhelperkeyboard.short_cut.ShortcutType import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import timber.log.Timber +import java.util.concurrent.Executor +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicInteger + +private class PreviewCandidateItemColorState { + var backgroundColor: Int? = null + private set + var pressedBackgroundColor: Int? = null + private set + + fun setColors(backgroundColor: Int, pressedBackgroundColor: Int): Boolean { + if ( + this.backgroundColor == backgroundColor && + this.pressedBackgroundColor == pressedBackgroundColor + ) { + return false + } + this.backgroundColor = backgroundColor + this.pressedBackgroundColor = pressedBackgroundColor + return true + } +} + +private data class PreviewCandidateYomiPresentation( + val isVisible: Boolean, + val text: String, + val textSize: Float +) + +private fun resolvePreviewCandidateYomiPresentation( + showCandidateYomiForLiveConversion: Boolean, + isFirstCandidate: Boolean, + suggestion: Candidate, + candidateTextSize: Float +): PreviewCandidateYomiPresentation { + val yomi = suggestion.yomi + val shouldShowYomi = + showCandidateYomiForLiveConversion && + isFirstCandidate && + !yomi.isNullOrBlank() && + yomi != suggestion.string + return PreviewCandidateYomiPresentation( + isVisible = shouldShowYomi, + text = if (shouldShowYomi) yomi.orEmpty() else "", + textSize = candidateTextSize * 0.72f + ) +} class SuggestionAdapter2 : RecyclerView.Adapter() { @@ -38,47 +95,202 @@ class SuggestionAdapter2 : RecyclerView.Adapter() { private const val VIEW_TYPE_EMPTY = 0 private const val VIEW_TYPE_SUGGESTION = 1 private const val VIEW_TYPE_CUSTOM_LAYOUT_PICKER = 2 + private const val VIEW_TYPE_GEMMA_ACTION = 3 + private const val VIEW_TYPE_SHORTCUT = 4 + + private val diffThreadIndex = AtomicInteger(0) + private val diffExecutor: Executor = Executors.newFixedThreadPool(2) { runnable -> + Thread(runnable, "SuggestionAdapter2Diff-${diffThreadIndex.incrementAndGet()}").apply { + isDaemon = true + } + } } enum class HelperIcon { - UNDO, PASTE + UNDO, REDO, RECONVERT, PASTE + } + + private sealed class SuggestionDisplayItem { + data class CandidateItem( + val candidate: Candidate, + val candidateIndex: Int, + ) : SuggestionDisplayItem() + + data class GemmaActionItem( + val candidate: Candidate, + val candidateIndex: Int, + ) : SuggestionDisplayItem() + + data class HelperActionsItem( + val state: HelperActionsState, + ) : SuggestionDisplayItem() + + data class ShortcutItem( + val shortcutType: ShortcutType, + ) : SuggestionDisplayItem() + + data class CustomLayoutItem( + val layout: CustomKeyboardLayout, + val layoutIndex: Int, + ) : SuggestionDisplayItem() + } + + private data class HelperActionsState( + val undoEnabled: Boolean, + val redoEnabled: Boolean, + val reconvertEnabled: Boolean, + val pasteEnabled: Boolean, + val clipboardDescriptionShown: Boolean, + val clipboardText: String, + val clipboardBitmap: Bitmap?, + val undoText: String, + val redoText: String, + val incognitoIconDrawable: android.graphics.drawable.Drawable?, + ) { + val hasClipboardPreview: Boolean + get() = pasteEnabled && (clipboardBitmap != null || clipboardText.isNotBlank()) + + val hasVisibleAction: Boolean + get() = undoEnabled || + redoEnabled || + reconvertEnabled || + pasteEnabled || + incognitoIconDrawable != null } - // Listeners for clicks private var onItemClickListener: ((Candidate, Int) -> Unit)? = null private var onItemLongClickListener: ((Candidate, Int) -> Unit)? = null private var onItemHelperIconClickListener: ((HelperIcon) -> Unit)? = null private var onItemHelperIconLongClickListener: ((HelperIcon) -> Unit)? = null private var onCustomLayoutItemClickListener: ((Int) -> Unit)? = null + private var onShortcutItemClickListener: ((ShortcutType) -> Unit)? = null private var onShowSoftKeyboardClick: (() -> Unit)? = null private val adapterScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) var onListUpdated: (() -> Unit)? = null - // Holds the preview content for the empty state. private var clipboardText: String = "" - private var clipboardBitmap: Bitmap? = null // ★追加: Bitmapを保持するフィールド + private var clipboardBitmap: Bitmap? = null private var undoText: String = "" - - // Internal flags to track enable/disable state + private var redoText: String = "" + private var isReconvertEnabled: Boolean = false private var isUndoEnabled: Boolean = false + private var isRedoEnabled: Boolean = false private var isPasteEnabled: Boolean = true + private var isClipboardDescriptionShow: Boolean = true private var currentMode: TenKeyQWERTYMode = TenKeyQWERTYMode.Default private var customLayouts: List = emptyList() - private var showCustomTab: Boolean = true + private var shortcutItems: List = emptyList() + private var showIntegratedShortcuts: Boolean = false + private var shortcutIconColor: Int? = null + private var activeShortcutTypes: Set = emptySet() + private var incognitoIconDrawable: android.graphics.drawable.Drawable? = null private var candidateTextSize: Float = 14f + private var candidateTextColor: Int? = null + private var showCandidateYomiForLiveConversion: Boolean = false + private val candidateItemColorState = PreviewCandidateItemColorState() + + private var candidateEmptyDrawableColor: Int? = null + private var candidateEmptyDrawableTextColor: Int? = null + private var released: Boolean = false + private var displayGeneration: Int = 0 + private var highlightedPosition: Int = RecyclerView.NO_POSITION + private var candidateSuggestions: List = emptyList() + + private val displayItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: SuggestionDisplayItem, + newItem: SuggestionDisplayItem + ): Boolean { + return when { + oldItem is SuggestionDisplayItem.CandidateItem && + newItem is SuggestionDisplayItem.CandidateItem -> + oldItem.candidateIndex == newItem.candidateIndex && + oldItem.candidate.string == newItem.candidate.string && + oldItem.candidate.type == newItem.candidate.type + + oldItem is SuggestionDisplayItem.GemmaActionItem && + newItem is SuggestionDisplayItem.GemmaActionItem -> + oldItem.candidateIndex == newItem.candidateIndex && + oldItem.candidate.string == newItem.candidate.string && + oldItem.candidate.type == newItem.candidate.type + + oldItem is SuggestionDisplayItem.HelperActionsItem && + newItem is SuggestionDisplayItem.HelperActionsItem -> true + + oldItem is SuggestionDisplayItem.ShortcutItem && + newItem is SuggestionDisplayItem.ShortcutItem -> + oldItem.shortcutType == newItem.shortcutType + + oldItem is SuggestionDisplayItem.CustomLayoutItem && + newItem is SuggestionDisplayItem.CustomLayoutItem -> + oldItem.layout.stableId == newItem.layout.stableId + + else -> false + } + } + + override fun areContentsTheSame( + oldItem: SuggestionDisplayItem, + newItem: SuggestionDisplayItem + ): Boolean = oldItem == newItem + } + + private val displayListUpdateCallback = object : ListUpdateCallback { + override fun onInserted(position: Int, count: Int) { + if (!released) notifyItemRangeInserted(position, count) + } + + override fun onRemoved(position: Int, count: Int) { + if (!released) notifyItemRangeRemoved(position, count) + } + + override fun onMoved(fromPosition: Int, toPosition: Int) { + if (!released) notifyItemMoved(fromPosition, toPosition) + } + + override fun onChanged(position: Int, count: Int, payload: Any?) { + if (!released) notifyItemRangeChanged(position, count, payload) + } + } + + private val differ = AsyncListDiffer( + displayListUpdateCallback, + AsyncDifferConfig.Builder(displayItemCallback) + .setBackgroundThreadExecutor { command -> + diffExecutor.execute(command) + } + .build() + ) + + private val displayItems: List + get() = differ.currentList + + var suggestions: List + get() = candidateSuggestions + set(value) { + if (candidateSuggestions == value) return + candidateSuggestions = value + rebuildDisplayItems { + onListUpdated?.invoke() + } + } + + init { + differ.submitList(buildDisplayItems()) + } fun setOnItemClickListener(onItemClick: (Candidate, Int) -> Unit) { - this.onItemClickListener = onItemClick + onItemClickListener = onItemClick } fun setOnItemLongClickListener(onItemLongClick: (Candidate, Int) -> Unit) { - this.onItemLongClickListener = onItemLongClick + onItemLongClickListener = onItemLongClick } fun setOnItemHelperIconClickListener(onItemHelperIconClickListener: (HelperIcon) -> Unit) { @@ -90,80 +302,138 @@ class SuggestionAdapter2 : RecyclerView.Adapter() { } fun setOnCustomLayoutItemClickListener(listener: (Int) -> Unit) { - this.onCustomLayoutItemClickListener = listener + onCustomLayoutItemClickListener = listener + } + + fun setOnShortcutItemClickListener(listener: (ShortcutType) -> Unit) { + onShortcutItemClickListener = listener } fun setOnPhysicalKeyboardListener(listener: () -> Unit) { - this.onShowSoftKeyboardClick = listener + onShowSoftKeyboardClick = listener } fun release() { + released = true onItemClickListener = null onItemLongClickListener = null onItemHelperIconClickListener = null onItemHelperIconLongClickListener = null onCustomLayoutItemClickListener = null + onShortcutItemClickListener = null onShowSoftKeyboardClick = null onListUpdated = null incognitoIconDrawable = null adapterScope.cancel() } - /** - * ★新しい関数: シークレットモードのアイコンを設定します。 - * Drawableがnullでなければアイコンを表示し、nullなら非表示にします。 - */ fun setIncognitoIcon(drawable: android.graphics.drawable.Drawable?) { - this.incognitoIconDrawable = drawable - if (suggestions.isEmpty()) { - notifyItemChanged(0) - } + if (incognitoIconDrawable === drawable) return + incognitoIconDrawable = drawable + rebuildDisplayItems() } fun setUndoEnabled(enabled: Boolean) { + if (isUndoEnabled == enabled) return isUndoEnabled = enabled - if (suggestions.isEmpty()) { - notifyItemChanged(0) - } + rebuildDisplayItems() } fun setPasteEnabled(enabled: Boolean) { + if (isPasteEnabled == enabled) return isPasteEnabled = enabled - if (suggestions.isEmpty()) { - notifyItemChanged(0) - } + rebuildDisplayItems() + } + + fun setRedoEnabled(enabled: Boolean) { + if (isRedoEnabled == enabled) return + isRedoEnabled = enabled + rebuildDisplayItems() + } + + fun setReconvertEnabled(enabled: Boolean) { + if (isReconvertEnabled == enabled) return + isReconvertEnabled = enabled + rebuildDisplayItems() + } + + fun setClipboardDescriptionTextVisibility(visibility: Boolean) { + if (isClipboardDescriptionShow == visibility) return + isClipboardDescriptionShow = visibility + rebuildDisplayItems() } - /** - * テキストのクリップボードプレビューを設定します。 - * このとき、画像のプレビューはクリアされます。 - */ fun setClipboardPreview(text: String) { + if (clipboardText == text && clipboardBitmap == null) return clipboardText = text - clipboardBitmap = null // ★追加: テキスト設定時に画像はクリア - if (suggestions.isEmpty()) { - notifyItemChanged(0) - } + clipboardBitmap = null + rebuildDisplayItems() } - /** - * ★新しい関数: 画像のクリップボードプレビューを設定します。 - * このとき、テキストのプレビューはクリアされます。 - */ fun setClipboardImagePreview(bitmap: Bitmap?) { + if (clipboardBitmap == bitmap && clipboardText.isEmpty()) return clipboardBitmap = bitmap - clipboardText = "" // 画像設定時にテキストはクリア - if (suggestions.isEmpty()) { - notifyItemChanged(0) + clipboardText = "" + rebuildDisplayItems() + } + + fun isShowingClipboardPreviewForEmptyState(): Boolean { + return currentHelperActionsState().hasClipboardPreview + } + + fun isShowingCustomLayoutPicker(): Boolean { + return currentMode is TenKeyQWERTYMode.Custom && customLayouts.isNotEmpty() && showCustomTab + } + + fun setShortcutItems(items: List) { + if (shortcutItems == items) return + shortcutItems = items + rebuildDisplayItems() + } + + fun setIntegratedShortcutVisibility(visible: Boolean) { + if (showIntegratedShortcuts == visible) return + showIntegratedShortcuts = visible + rebuildDisplayItems() + } + + fun setShortcutIconColor(color: Int) { + if (shortcutIconColor == color) return + shortcutIconColor = color + if (showIntegratedShortcuts && suggestions.isEmpty()) { + notifyItemRangeChanged(0, itemCount) + } + } + + fun setActiveShortcutTypes(activeTypes: Set) { + if (activeShortcutTypes == activeTypes) return + val oldActive = activeShortcutTypes + activeShortcutTypes = activeTypes + (oldActive union activeTypes).forEach { type -> + notifyShortcutItemChanged(type) } } + fun setKeyboardLayoutEditActive(active: Boolean) { + setActiveShortcutTypes( + if (active) { + activeShortcutTypes + ShortcutType.KEYBOARD_LAYOUT_EDIT + } else { + activeShortcutTypes - ShortcutType.KEYBOARD_LAYOUT_EDIT + } + ) + } fun setUndoPreviewText(text: String) { + if (undoText == text) return undoText = text - if (suggestions.isEmpty()) { - notifyItemChanged(0) - } + rebuildDisplayItems() + } + + fun setRedoPreviewText(text: String) { + if (redoText == text) return + redoText = text + rebuildDisplayItems() } fun updateState(mode: TenKeyQWERTYMode, layouts: List) { @@ -171,44 +441,93 @@ class SuggestionAdapter2 : RecyclerView.Adapter() { currentMode = mode customLayouts = layouts if (needsFullRefresh) { - notifyItemChanged(layouts.size) + rebuildDisplayItems() } } fun updateCustomTabVisibility(visibility: Boolean) { + if (showCustomTab == visibility) return showCustomTab = visibility + rebuildDisplayItems() } - private val diffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: Candidate, newItem: Candidate): Boolean { - return oldItem.string == newItem.string - } + private fun rebuildDisplayItems(onCommitted: (() -> Unit)? = null) { + if (released) return + val newItems = buildDisplayItems() + if (displayItems == newItems) return - override fun areContentsTheSame(oldItem: Candidate, newItem: Candidate): Boolean { - return oldItem == newItem + val generation = ++displayGeneration + differ.submitList(newItems) { + if (released || generation != displayGeneration) return@submitList + onCommitted?.invoke() } } - private val differ = AsyncListDiffer(this, diffCallback) - var suggestions: List - get() = differ.currentList - set(value) { - // submitListの第2引数にコールバックを渡す - differ.submitList(value) { - onListUpdated?.invoke() + private fun buildDisplayItems(): List { + if (candidateSuggestions.isNotEmpty()) { + return candidateSuggestions.mapIndexed { index, candidate -> + if (candidate.isSelectedTextGemmaActionCandidate()) { + SuggestionDisplayItem.GemmaActionItem(candidate, index) + } else { + SuggestionDisplayItem.CandidateItem(candidate, index) + } } } - private var highlightedPosition: Int = RecyclerView.NO_POSITION + if (isShowingCustomLayoutPicker()) { + return customLayouts.mapIndexed { index, layout -> + SuggestionDisplayItem.CustomLayoutItem(layout, index) + } + } + + return buildList { + val helperState = currentHelperActionsState() + if (helperState.hasVisibleAction) { + add(SuggestionDisplayItem.HelperActionsItem(helperState)) + } + if (shouldShowIntegratedShortcuts()) { + shortcutItems.forEach { shortcutType -> + add(SuggestionDisplayItem.ShortcutItem(shortcutType)) + } + } + } + } + + private fun currentHelperActionsState(): HelperActionsState = + HelperActionsState( + undoEnabled = isUndoEnabled, + redoEnabled = isRedoEnabled, + reconvertEnabled = isReconvertEnabled, + pasteEnabled = isPasteEnabled, + clipboardDescriptionShown = isClipboardDescriptionShow, + clipboardText = clipboardText, + clipboardBitmap = clipboardBitmap, + undoText = undoText, + redoText = redoText, + incognitoIconDrawable = incognitoIconDrawable, + ) inner class SuggestionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val text: MaterialTextView = itemView.findViewById(R.id.suggestion_item_text_view) + val yomiText: MaterialTextView = itemView.findViewById(R.id.suggestion_item_yomi_text_view) val typeText: MaterialTextView = itemView.findViewById(R.id.suggestion_item_type_text_view) } + inner class GemmaActionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val badgeText: MaterialTextView = itemView.findViewById(R.id.suggestion_gemma_action_badge) + val actionText: MaterialTextView = itemView.findViewById(R.id.suggestion_gemma_action_text) + } + inner class EmptyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val undoIconParent: ConstraintLayout? = itemView.findViewById(R.id.undo_icon_parent) + val undoImageView: ImageView? = itemView.findViewById(R.id.imageView) val undoIcon: MaterialTextView? = itemView.findViewById(R.id.undo_icon) + val redoIconParent: ConstraintLayout? = itemView.findViewById(R.id.redo_icon_parent) + val redoImageView: ImageView? = itemView.findViewById(R.id.redo_image_view) + val redoIcon: MaterialTextView? = itemView.findViewById(R.id.redo_icon) + val reconvertIconParent: ConstraintLayout? = itemView.findViewById(R.id.reconvert_icon_parent) + val reconvertIcon: MaterialTextView? = itemView.findViewById(R.id.reconvert_icon) + val reconvertImageView: ImageView? = itemView.findViewById(R.id.reconvert_image_view) val pasteIconParent: ConstraintLayout? = itemView.findViewById(R.id.paste_icon_patent) val pasteIcon: ImageView? = itemView.findViewById(R.id.paste_icon) val clipboardPreviewText: MaterialTextView? = @@ -222,30 +541,34 @@ class SuggestionAdapter2 : RecyclerView.Adapter() { val nameTextView: MaterialTextView = itemView.findViewById(R.id.custom_layout_name) } + inner class ShortcutViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val imageView: ImageView = itemView.findViewById(R.id.item_image) + } + override fun getItemViewType(position: Int): Int { - return if (suggestions.isNotEmpty()) { - VIEW_TYPE_SUGGESTION - } else { - if (currentMode is TenKeyQWERTYMode.Custom && customLayouts.isNotEmpty() && showCustomTab) { - VIEW_TYPE_CUSTOM_LAYOUT_PICKER - } else { - VIEW_TYPE_EMPTY - } + return when (displayItems[position]) { + is SuggestionDisplayItem.CandidateItem -> VIEW_TYPE_SUGGESTION + is SuggestionDisplayItem.GemmaActionItem -> VIEW_TYPE_GEMMA_ACTION + is SuggestionDisplayItem.HelperActionsItem -> VIEW_TYPE_EMPTY + is SuggestionDisplayItem.ShortcutItem -> VIEW_TYPE_SHORTCUT + is SuggestionDisplayItem.CustomLayoutItem -> VIEW_TYPE_CUSTOM_LAYOUT_PICKER } } - override fun getItemCount(): Int { - return if (suggestions.isNotEmpty()) { - suggestions.size - } else { - if (currentMode is TenKeyQWERTYMode.Custom && customLayouts.isNotEmpty()) { - customLayouts.size - } else { - suggestions.size - } + fun isFullSpanItem(position: Int): Boolean { + return when (getItemViewType(position)) { + VIEW_TYPE_EMPTY, + VIEW_TYPE_CUSTOM_LAYOUT_PICKER, + VIEW_TYPE_SHORTCUT -> true + + else -> false } } + override fun getItemCount(): Int { + return displayItems.size + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val isDynamicColorEnable = DynamicColors.isDynamicColorAvailable() return when (viewType) { @@ -265,82 +588,145 @@ class SuggestionAdapter2 : RecyclerView.Adapter() { val itemView = LayoutInflater.from(parent.context) .inflate(R.layout.suggestion_item, parent, false) itemView.setBackgroundResource( - if (isDynamicColorEnable) com.kazumaproject.core.R.drawable.recyclerview_item_bg_material else com.kazumaproject.core.R.drawable.recyclerview_item_bg + if (isDynamicColorEnable) { + com.kazumaproject.core.R.drawable.recyclerview_item_bg_material + } else { + com.kazumaproject.core.R.drawable.recyclerview_item_bg + } ) SuggestionViewHolder(itemView) } + VIEW_TYPE_GEMMA_ACTION -> { + val itemView = LayoutInflater.from(parent.context) + .inflate(R.layout.suggestion_gemma_action_item, parent, false) + itemView.setBackgroundResource( + if (isDynamicColorEnable) { + com.kazumaproject.core.R.drawable.recyclerview_item_bg_material + } else { + com.kazumaproject.core.R.drawable.recyclerview_item_bg + } + ) + GemmaActionViewHolder(itemView) + } + + VIEW_TYPE_SHORTCUT -> { + val itemView = LayoutInflater.from(parent.context) + .inflate(R.layout.item_shortcut, parent, false) + ShortcutViewHolder(itemView) + } + else -> throw IllegalArgumentException("Unknown view type: $viewType") } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = displayItems.getOrNull(position) ?: return when (holder.itemViewType) { - VIEW_TYPE_EMPTY -> onBindEmptyViewHolder(holder as EmptyViewHolder) + VIEW_TYPE_EMPTY -> onBindEmptyViewHolder( + holder as EmptyViewHolder, + (item as SuggestionDisplayItem.HelperActionsItem).state, + ) + VIEW_TYPE_SUGGESTION -> onBindSuggestionViewHolder( - holder as SuggestionViewHolder, position + holder as SuggestionViewHolder, + item as SuggestionDisplayItem.CandidateItem, + ) + + VIEW_TYPE_GEMMA_ACTION -> onBindGemmaActionViewHolder( + holder as GemmaActionViewHolder, + item as SuggestionDisplayItem.GemmaActionItem, + ) + + VIEW_TYPE_SHORTCUT -> onBindShortcutViewHolder( + holder as ShortcutViewHolder, + item as SuggestionDisplayItem.ShortcutItem, ) VIEW_TYPE_CUSTOM_LAYOUT_PICKER -> onBindCustomLayoutViewHolder( - holder as CustomLayoutViewHolder, position + holder as CustomLayoutViewHolder, + item as SuggestionDisplayItem.CustomLayoutItem, ) } } - private fun onBindEmptyViewHolder(holder: EmptyViewHolder) { + private fun onBindEmptyViewHolder(holder: EmptyViewHolder, state: HelperActionsState) { val isDynamicColorEnable = DynamicColors.isDynamicColorAvailable() + Timber.d("SuggestionAdapter2 onBindEmptyViewHolder: ${state.clipboardText} ${state.pasteEnabled}") holder.apply { incognitoIcon?.apply { - if (incognitoIconDrawable != null) { + if (state.incognitoIconDrawable != null) { visibility = View.VISIBLE - setImageDrawable(incognitoIconDrawable) + setImageDrawable(state.incognitoIconDrawable) } else { visibility = View.GONE } } undoIcon?.apply { - isVisible = isUndoEnabled + isVisible = state.undoEnabled + isFocusable = false + Timber.d("undo text: ${state.undoText}") + debugPrintCodePoints(state.undoText) + text = state.undoText + } + redoIcon?.apply { + isVisible = state.redoEnabled + isFocusable = false + text = state.redoText + } + reconvertIcon?.apply { + isVisible = state.reconvertEnabled isFocusable = false - Timber.d("undo text: $undoText") - debugPrintCodePoints(undoText) - text = undoText.reversed() } pasteIconParent?.apply { - isEnabled = isPasteEnabled - visibility = if (isPasteEnabled) View.VISIBLE else View.INVISIBLE + isEnabled = state.pasteEnabled + visibility = if (state.pasteEnabled) View.VISIBLE else View.GONE isFocusable = false } - // ★修正: 画像プレビューのロジック pasteIcon?.apply { - if (clipboardBitmap != null) { - // Bitmapがあればそれを設定 - setImageBitmap(clipboardBitmap) + if (state.clipboardBitmap != null) { + setImageBitmap(state.clipboardBitmap) + clearColorFilter() scaleType = ImageView.ScaleType.CENTER_CROP } else { - // なければデフォルトのアイコンを設定 setImageResource(com.kazumaproject.core.R.drawable.content_paste_24px) scaleType = ImageView.ScaleType.CENTER_INSIDE } } - // テキストプレビューは、画像がない場合にのみ表示 - clipboardPreviewText?.text = if (clipboardBitmap == null) clipboardText else "" + clipboardPreviewText?.text = + if (state.clipboardBitmap == null) state.clipboardText else "" + + applyEmptyHelperButtonStyle( + parent = undoIconParent, + text = undoIcon, + icon = undoImageView, + isDynamicColorEnable = isDynamicColorEnable, + ) + applyEmptyHelperButtonStyle( + parent = redoIconParent, + text = redoIcon, + icon = redoImageView, + isDynamicColorEnable = isDynamicColorEnable, + ) + applyEmptyHelperButtonStyle( + parent = reconvertIconParent, + text = reconvertIcon, + icon = reconvertImageView, + isDynamicColorEnable = isDynamicColorEnable, + ) + applyEmptyHelperButtonStyle( + parent = pasteIconParent, + text = clipboardPreviewText, + icon = if (state.clipboardBitmap == null) pasteIcon else null, + isDynamicColorEnable = isDynamicColorEnable, + ) + applyEmptyHelperTextColor(clipboardPreviewTextDescription) undoIconParent?.apply { - if (isDynamicColorEnable) { - if (this.context.isDarkThemeOn()) { - setBackgroundResource( - com.kazumaproject.core.R.drawable.ten_keys_side_bg_material - ) - } else { - setBackgroundResource( - com.kazumaproject.core.R.drawable.ten_keys_side_bg_material_light - ) - } - } - isVisible = isUndoEnabled + isVisible = state.undoEnabled setOnClickListener { onItemHelperIconClickListener?.invoke(HelperIcon.UNDO) } @@ -350,8 +736,28 @@ class SuggestionAdapter2 : RecyclerView.Adapter() { } } - // テキスト用の説明は、画像がない場合にのみ表示 - clipboardPreviewTextDescription?.isVisible = isPasteEnabled && clipboardBitmap == null + redoIconParent?.apply { + isVisible = state.redoEnabled + setOnClickListener { + onItemHelperIconClickListener?.invoke(HelperIcon.REDO) + } + setOnLongClickListener { + onItemHelperIconLongClickListener?.invoke(HelperIcon.REDO) + true + } + } + + reconvertIconParent?.apply { + isVisible = state.reconvertEnabled + setOnClickListener { + onItemHelperIconClickListener?.invoke(HelperIcon.RECONVERT) + } + setOnLongClickListener { + false + } + } + + clipboardPreviewTextDescription?.isVisible = false pasteIconParent?.apply { setOnClickListener { @@ -365,14 +771,150 @@ class SuggestionAdapter2 : RecyclerView.Adapter() { } } + private fun applyEmptyHelperButtonStyle( + parent: ConstraintLayout?, + text: MaterialTextView?, + icon: ImageView?, + isDynamicColorEnable: Boolean, + ) { + applyEmptyHelperButtonBackground(parent, isDynamicColorEnable) + applyEmptyHelperTextColor(text) + candidateEmptyDrawableTextColor?.let { color -> + icon?.setColorFilter(color, PorterDuff.Mode.SRC_IN) + } ?: icon?.clearColorFilter() + } + + private fun applyEmptyHelperTextColor(text: MaterialTextView?) { + text ?: return + text.setTextColor( + candidateEmptyDrawableTextColor ?: ContextCompat.getColor( + text.context, + com.kazumaproject.core.R.color.keyboard_icon_color, + ) + ) + } + + private fun applyEmptyHelperButtonBackground( + parent: ConstraintLayout?, + isDynamicColorEnable: Boolean, + ) { + parent ?: return + val customBackgroundColor = candidateEmptyDrawableColor + if (customBackgroundColor != null) { + parent.setBackgroundResource(com.kazumaproject.core.R.drawable.ten_keys_center_bg) + parent.setDrawableSolidColor(customBackgroundColor) + return + } + if (isDynamicColorEnable) { + parent.setBackgroundResource( + if (parent.context.isDarkThemeOn()) { + com.kazumaproject.core.R.drawable.ten_keys_side_bg_material + } else { + com.kazumaproject.core.R.drawable.ten_keys_side_bg_material_light + } + ) + } else { + parent.setBackgroundResource(com.kazumaproject.core.R.drawable.ten_keys_center_bg) + } + } + + private fun shouldShowIntegratedShortcuts(): Boolean { + return showIntegratedShortcuts && + shortcutItems.isNotEmpty() && + !isShowingCustomLayoutPicker() && + !isShowingClipboardPreviewForEmptyState() + } + + private fun onBindShortcutViewHolder( + holder: ShortcutViewHolder, + item: SuggestionDisplayItem.ShortcutItem, + ) { + val shortcutType = item.shortcutType + holder.imageView.apply { + setImageResource(shortcutType.resolveShortcutIconResId()) + contentDescription = shortcutType.description + shortcutIconColor?.let { color -> + setColorFilter(color, PorterDuff.Mode.SRC_IN) + } ?: clearColorFilter() + } + holder.itemView.contentDescription = shortcutType.description + holder.itemView.setOnClickListener { + val adapterPosition = holder.bindingAdapterPosition + if (adapterPosition != RecyclerView.NO_POSITION) { + val currentItem = displayItems.getOrNull(adapterPosition) + if (currentItem is SuggestionDisplayItem.ShortcutItem) { + onShortcutItemClickListener?.invoke(currentItem.shortcutType) + } + } + } + } + + private fun ShortcutType.resolveShortcutIconResId(): Int { + return if (this in activeShortcutTypes) { + activeIconResId ?: iconResId + } else { + iconResId + } + } + + private fun notifyShortcutItemChanged(shortcutType: ShortcutType) { + val index = displayItems.indexOfFirst { + it is SuggestionDisplayItem.ShortcutItem && it.shortcutType == shortcutType + } + if (index >= 0) { + notifyItemChanged(index) + } + } + fun setCandidateTextSize(size: Float) { if (candidateTextSize == size) return candidateTextSize = size notifyItemRangeChanged(0, itemCount) } - private fun onBindSuggestionViewHolder(holder: SuggestionViewHolder, position: Int) { - val suggestion = suggestions[position] + fun setShowCandidateYomiForLiveConversion(enabled: Boolean) { + if (showCandidateYomiForLiveConversion == enabled) return + showCandidateYomiForLiveConversion = enabled + notifyItemRangeChanged(0, itemCount) + } + + fun setCandidateTextColor(color: Int) { + if (candidateTextColor == color) return + candidateTextColor = color + notifyItemRangeChanged(0, itemCount) + } + + fun setCandidateItemColors(backgroundColor: Int, pressedColor: Int) { + if (!candidateItemColorState.setColors(backgroundColor, pressedColor)) return + notifyItemRangeChanged(0, itemCount) + } + + fun setCandidateEmptyPopupColors(backgroundColor: Int, textColor: Int) { + if ( + candidateEmptyDrawableColor == backgroundColor && + candidateEmptyDrawableTextColor == textColor + ) { + return + } + candidateEmptyDrawableColor = backgroundColor + candidateEmptyDrawableTextColor = textColor + notifyItemRangeChanged(0, itemCount) + } + + fun clearCandidateEmptyPopupColors() { + if (candidateEmptyDrawableColor == null && candidateEmptyDrawableTextColor == null) return + candidateEmptyDrawableColor = null + candidateEmptyDrawableTextColor = null + notifyItemRangeChanged(0, itemCount) + } + + private fun onBindSuggestionViewHolder( + holder: SuggestionViewHolder, + item: SuggestionDisplayItem.CandidateItem, + ) { + applyCandidateItemBackground(holder.itemView) + val suggestion = item.candidate + val position = item.candidateIndex val paddingLength = when { position == 0 -> 4 suggestion.string.length == 1 -> 4 @@ -392,20 +934,35 @@ class SuggestionAdapter2 : RecyclerView.Adapter() { } holder.text.textSize = candidateTextSize + val yomiPresentation = resolvePreviewCandidateYomiPresentation( + showCandidateYomiForLiveConversion = showCandidateYomiForLiveConversion, + isFirstCandidate = position == 0, + suggestion = suggestion, + candidateTextSize = candidateTextSize + ) + holder.yomiText.isVisible = yomiPresentation.isVisible + holder.yomiText.text = yomiPresentation.text + holder.yomiText.textSize = yomiPresentation.textSize + holder.yomiText.translationX = if (yomiPresentation.isVisible) { + holder.text.paint.measureText(" ".repeat(paddingLength)) + } else { + 0f + } + + candidateTextColor?.let { color -> + holder.text.setTextColor(color) + holder.typeText.setTextColor(color) + holder.yomiText.setTextColor(color) + } holder.typeText.text = when (suggestion.type) { (1).toByte() -> "" - /** 予測 **/ (9).toByte() -> "" (5).toByte() -> "[部]" (7).toByte() -> "" - /** 最長 **/ (10).toByte() -> "" - /** 絵文字 **/ (11).toByte() -> " " - /** 顔文字 **/ (12).toByte() -> " " - /** 記号 **/ (13).toByte() -> { when { suggestion.string.isAllHalfWidthNumericSymbol() -> "[半]" @@ -413,9 +970,7 @@ class SuggestionAdapter2 : RecyclerView.Adapter() { else -> " " } } - /** 日付 **/ (14).toByte() -> "[日付]" - /** 修正 **/ (15).toByte() -> { val spannable = SpannableString("[読] ${readingCorrectionString.second}") spannable.setSpan( @@ -423,43 +978,36 @@ class SuggestionAdapter2 : RecyclerView.Adapter() { ) spannable } - /** ことわざ **/ (16).toByte() -> "" - /** 数 漢字混じり **/ (17).toByte() -> "" - /** 数 カンマあり**/ (18).toByte() -> "" - /** 数 **/ (19).toByte() -> "" - /** 学習 **/ (20).toByte() -> "" - /** 記号 **/ (21).toByte() -> when { suggestion.string.isAllHalfWidthNumericSymbol() -> "[半]" suggestion.string.isAllFullWidthNumericSymbol() -> "[全]" else -> " " } - /** 全角数字 **/ (22).toByte() -> "[全]" - /** Mozc UT Names **/ (23).toByte() -> "" - /** Mozc UT Places **/ (24).toByte() -> "" - /** Mozc UT Wiki **/ (25).toByte() -> "" - /** Mozc UT Neologd **/ (26).toByte() -> "" - /** Mozc UT Web **/ (27).toByte() -> "" (28).toByte() -> "" - /** 英語 **/ (29).toByte() -> "" - /** 全角 **/ (30).toByte() -> "[全]" - /** 半角 **/ (31).toByte() -> "[半]" - /** 漢数字 **/ (32).toByte() -> "" + (33).toByte() -> "[AI]" + (34).toByte() -> "[履歴]" + (35).toByte() -> "[修正]" + (36).toByte() -> "" + (37).toByte() -> "[AI]" + (38).toByte() -> "" + (39).toByte() -> "" + (40).toByte() -> "[AI]" + QWERTY_GLIDE_CANDIDATE_TYPE -> "" GemmaTranslationManager.TRANSLATED_CANDIDATE_TYPE.toByte() -> "[訳]" GemmaTranslationManager.PROMPT_RESULT_CANDIDATE_TYPE.toByte() -> "[AI]" GemmaTranslationManager.SELECTION_TRANSLATE_ACTION_CANDIDATE_TYPE.toByte() -> "[訳]" @@ -476,11 +1024,87 @@ class SuggestionAdapter2 : RecyclerView.Adapter() { } } - private fun onBindCustomLayoutViewHolder(holder: CustomLayoutViewHolder, position: Int) { - val layoutItem = customLayouts[position] - holder.nameTextView.text = layoutItem.name + private fun onBindGemmaActionViewHolder( + holder: GemmaActionViewHolder, + item: SuggestionDisplayItem.GemmaActionItem, + ) { + applyCandidateItemBackground(holder.itemView) + val suggestion = item.candidate + val position = item.candidateIndex + holder.actionText.text = suggestion.string + holder.actionText.textSize = candidateTextSize + holder.badgeText.text = when (suggestion.type) { + GemmaTranslationManager.SELECTION_TRANSLATE_ACTION_CANDIDATE_TYPE.toByte() -> "訳" + GemmaTranslationManager.SELECTION_PROMPT_ACTION_CANDIDATE_TYPE.toByte() -> "AI" + else -> "" + } + + candidateTextColor?.let { color -> + holder.actionText.setTextColor(color) + holder.badgeText.setTextColor(color) + } + + holder.itemView.isPressed = position == highlightedPosition + holder.itemView.setOnClickListener { + onItemClickListener?.invoke(suggestion, position) + } + holder.itemView.setOnLongClickListener { + onItemLongClickListener?.invoke(suggestion, position) + true + } + } + + private fun onBindCustomLayoutViewHolder( + holder: CustomLayoutViewHolder, + item: SuggestionDisplayItem.CustomLayoutItem, + ) { + holder.nameTextView.text = item.layout.name holder.itemView.setOnClickListener { - onCustomLayoutItemClickListener?.invoke(position) + onCustomLayoutItemClickListener?.invoke(item.layoutIndex) + } + } + + private fun applyCandidateItemBackground(itemView: View) { + val backgroundColor = candidateItemColorState.backgroundColor + val pressedColor = candidateItemColorState.pressedBackgroundColor + if (backgroundColor == null && pressedColor == null) { + itemView.setBackgroundResource(defaultCandidateItemBackgroundRes()) + return + } + + itemView.background = StateListDrawable().apply { + addState( + intArrayOf(android.R.attr.state_pressed), + createCandidateItemDrawable( + pressedColor ?: ContextCompat.getColor( + itemView.context, + com.kazumaproject.core.R.color.qwety_key_bg_color + ), + itemView.context.resources.displayMetrics.density + ) + ) + addState( + intArrayOf(), + createCandidateItemDrawable( + backgroundColor ?: Color.TRANSPARENT, + itemView.context.resources.displayMetrics.density + ) + ) + } + } + + private fun defaultCandidateItemBackgroundRes(): Int { + return if (DynamicColors.isDynamicColorAvailable()) { + com.kazumaproject.core.R.drawable.recyclerview_item_bg_material + } else { + com.kazumaproject.core.R.drawable.recyclerview_item_bg + } + } + + private fun createCandidateItemDrawable(color: Int, density: Float): GradientDrawable { + return GradientDrawable().apply { + setColor(color) + cornerRadius = 16f * density } } @@ -488,8 +1112,28 @@ class SuggestionAdapter2 : RecyclerView.Adapter() { val previous = highlightedPosition highlightedPosition = newPosition if (previous != RecyclerView.NO_POSITION) { - notifyItemChanged(previous) + notifyCandidateDisplayItemChanged(previous) + } + if (highlightedPosition != RecyclerView.NO_POSITION) { + notifyCandidateDisplayItemChanged(highlightedPosition) } - notifyItemChanged(highlightedPosition) + } + + private fun notifyCandidateDisplayItemChanged(candidateIndex: Int) { + val displayIndex = displayItems.indexOfFirst { item -> + when (item) { + is SuggestionDisplayItem.CandidateItem -> item.candidateIndex == candidateIndex + is SuggestionDisplayItem.GemmaActionItem -> item.candidateIndex == candidateIndex + else -> false + } + } + if (displayIndex != -1) { + notifyItemChanged(displayIndex) + } + } + + private fun Candidate.isSelectedTextGemmaActionCandidate(): Boolean { + return type == GemmaTranslationManager.SELECTION_TRANSLATE_ACTION_CANDIDATE_TYPE.toByte() || + type == GemmaTranslationManager.SELECTION_PROMPT_ACTION_CANDIDATE_TYPE.toByte() } } diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/key_candidate_letter_size/KeyCandidateLetterSizeFragment.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/key_candidate_letter_size/KeyCandidateLetterSizeFragment.kt index 554ae1e4..34b94991 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/key_candidate_letter_size/KeyCandidateLetterSizeFragment.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/key_candidate_letter_size/KeyCandidateLetterSizeFragment.kt @@ -152,6 +152,7 @@ class KeyCandidateLetterSizeFragment : Fragment() { menuHost.addMenuProvider(object : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.fragment_reset_menu, menu) + menu.findItem(R.id.action_candidate_default_height)?.isVisible = false } override fun onMenuItemSelected(menuItem: MenuItem): Boolean { diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/CommonPreferenceFragment.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/CommonPreferenceFragment.kt index 3d6e55be..27764094 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/CommonPreferenceFragment.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/CommonPreferenceFragment.kt @@ -326,19 +326,22 @@ open class CommonPreferenceFragment : PreferenceFragmentCompat() { candidateColumnListPreference?.apply { setOnPreferenceChangeListener { _, newValue -> if (newValue is String) { - when (newValue) { - "1" -> { - appPreference.candidate_view_height_dp = 110 - } - - "2" -> { - appPreference.candidate_view_height_dp = 165 - } - - "3" -> { - appPreference.candidate_view_height_dp = 230 - } - } + appPreference.setCandidateColumnAndSyncHeight( + isLandscape = false, + column = newValue + ) + } + true + } + } + + findPreference("candidate_column_landscape_preference")?.apply { + setOnPreferenceChangeListener { _, newValue -> + if (newValue is String) { + appPreference.setCandidateColumnAndSyncHeight( + isLandscape = true, + column = newValue + ) } true } diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/SettingCardEditorController.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/SettingCardEditorController.kt index 87210f2f..2001911c 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/SettingCardEditorController.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/SettingCardEditorController.kt @@ -336,17 +336,23 @@ class SettingCardEditorController( }.getOrDefault(defaultValue) private fun writeStringPreference(preferenceKey: String, value: String) { + if (preferenceKey == "candidate_column_preference") { + appPreference.setCandidateColumnAndSyncHeight( + isLandscape = false, + column = value + ) + return + } + if (preferenceKey == "candidate_column_landscape_preference") { + appPreference.setCandidateColumnAndSyncHeight( + isLandscape = true, + column = value + ) + return + } preferences.edit() .putString(preferenceKey, value) .apply() - if (preferenceKey == "candidate_column_preference") { - appPreference.candidate_view_height_dp = when (value) { - "1" -> 110 - "2" -> 165 - "3" -> 230 - else -> appPreference.candidate_view_height_dp ?: 110 - } - } } private fun readIntPreference(preferenceKey: String, defaultValue: Int): Int = diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/SettingHomeFragment.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/SettingHomeFragment.kt index c9983088..a8f91f5c 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/SettingHomeFragment.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/setting/SettingHomeFragment.kt @@ -394,15 +394,11 @@ class SettingHomeFragment : Fragment() { appPreference.keyboard_width_landscape ?: 100, ) - "candidate_view_height_setting_fragment_preference" -> getString( - R.string.setting_home_dp_summary, - appPreference.candidate_view_height_dp ?: 110, - ) + "candidate_view_height_setting_fragment_preference" -> + candidateHeightSummary(isLandscape = false) - "candidate_view_height_landscape_setting_fragment_preference" -> getString( - R.string.setting_home_dp_summary, - appPreference.candidate_view_height_dp_landscape ?: 110, - ) + "candidate_view_height_landscape_setting_fragment_preference" -> + candidateHeightSummary(isLandscape = true) "setting_route_keyboard_theme" -> getString( R.string.setting_home_current_value, @@ -447,6 +443,22 @@ class SettingHomeFragment : Fragment() { } } + private fun candidateHeightSummary(isLandscape: Boolean): String { + appPreference.migrateCandidateHeightPerColumnPreferencesIfNeeded() + val currentColumn = appPreference.getCandidateColumn(isLandscape) + val activeHeight = appPreference.getCandidateVisibleHeightDp(isLandscape, currentColumn) + val candidateTextSize = appPreference.candidate_letter_size ?: 14.0f + return getString( + R.string.setting_home_candidate_height_summary, + appPreference.getCandidateVisibleHeightDp(isLandscape, "1"), + appPreference.getCandidateVisibleHeightDp(isLandscape, "2"), + appPreference.getCandidateVisibleHeightDp(isLandscape, "3"), + currentColumn.toIntOrNull() ?: 1, + activeHeight, + candidateTextSize, + ) + } + private fun currentValueText(destination: SettingDestination): String? { return settingCardEditorController.currentValueLabel(destination) ?.let { getString(R.string.setting_home_current_value, it) } diff --git a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/tenkey_letter_size_setting/TenKeyCandidateLetterSizeFragment.kt b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/tenkey_letter_size_setting/TenKeyCandidateLetterSizeFragment.kt index 505723b3..78c9243f 100644 --- a/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/tenkey_letter_size_setting/TenKeyCandidateLetterSizeFragment.kt +++ b/app/src/main/java/com/kazumaproject/markdownhelperkeyboard/setting_activity/ui/tenkey_letter_size_setting/TenKeyCandidateLetterSizeFragment.kt @@ -307,6 +307,7 @@ class TenKeyCandidateLetterSizeFragment : Fragment() { menuHost.addMenuProvider(object : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.fragment_reset_menu, menu) + menu.findItem(R.id.action_candidate_default_height)?.isVisible = false } override fun onMenuItemSelected(menuItem: MenuItem): Boolean { diff --git a/app/src/main/res/layout/fragment_candidate_height_landscape_setting.xml b/app/src/main/res/layout/fragment_candidate_height_landscape_setting.xml index b2a017fa..309a2f39 100644 --- a/app/src/main/res/layout/fragment_candidate_height_landscape_setting.xml +++ b/app/src/main/res/layout/fragment_candidate_height_landscape_setting.xml @@ -1,111 +1,518 @@ - -