From 548972e72f41daa600a0309faba77b00f08d6f29 Mon Sep 17 00:00:00 2001 From: Songbum Bae Date: Mon, 20 Apr 2026 18:17:20 +0900 Subject: [PATCH 01/20] =?UTF-8?q?feat:=20=EC=84=A4=EC=A0=95=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EB=B0=8F=20=EB=AA=A8=EC=95=84=ED=82=A4=20=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 설정 화면(SettingsActivity) 추가: 한글 입력 모드, 키패드 높이, 한손 모드 설정 - 모아키 레이아웃(open_moa_view_moakey) 추가 및 OpenMoaView에서 모드 전환 지원 - LanguageKey 길게 누르면 설정 화면 열기(OPEN_SETTINGS 특수키 추가) - HangulInputMode, KeypadHeight, OneHandMode config 클래스 추가 - SettingsPreferences로 SharedPreferences 접근 일원화 --- app/build.gradle | 5 +- app/src/main/AndroidManifest.xml | 5 + .../main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt | 48 +- .../pe/aioo/openmoa/config/HangulInputMode.kt | 13 + .../pe/aioo/openmoa/config/KeypadHeight.kt | 14 + .../pe/aioo/openmoa/config/OneHandMode.kt | 18 + .../aioo/openmoa/settings/SettingsActivity.kt | 95 ++++ .../openmoa/settings/SettingsPreferences.kt | 39 ++ .../aioo/openmoa/view/KeyboardFrameLayout.kt | 2 +- .../openmoa/view/keyboardview/OpenMoaView.kt | 237 +++++++--- .../LanguageKeyTouchListener.kt | 57 +++ .../aioo/openmoa/view/message/SpecialKey.kt | 1 + app/src/main/res/layout/activity_settings.xml | 132 ++++++ app/src/main/res/layout/open_moa_ime.xml | 13 +- app/src/main/res/layout/open_moa_view.xml | 64 ++- .../main/res/layout/open_moa_view_moakey.xml | 437 ++++++++++++++++++ app/src/main/res/values/strings.xml | 25 +- app/src/main/res/values/themes.xml | 10 + gradle.properties | 2 +- 19 files changed, 1120 insertions(+), 97 deletions(-) create mode 100644 app/src/main/kotlin/pe/aioo/openmoa/config/HangulInputMode.kt create mode 100644 app/src/main/kotlin/pe/aioo/openmoa/config/KeypadHeight.kt create mode 100644 app/src/main/kotlin/pe/aioo/openmoa/config/OneHandMode.kt create mode 100644 app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsActivity.kt create mode 100644 app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsPreferences.kt create mode 100644 app/src/main/kotlin/pe/aioo/openmoa/view/keytouchlistener/LanguageKeyTouchListener.kt create mode 100644 app/src/main/res/layout/activity_settings.xml create mode 100644 app/src/main/res/layout/open_moa_view_moakey.xml create mode 100644 app/src/main/res/values/themes.xml diff --git a/app/build.gradle b/app/build.gradle index 161057c..acec345 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -30,8 +30,9 @@ android { kotlinOptions { jvmTarget = '1.8' } - viewBinding { - enabled = true + buildFeatures { + viewBinding true + buildConfig true } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 53eec17..7af4741 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,6 +29,11 @@ + + \ No newline at end of file diff --git a/app/src/main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt b/app/src/main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt index e8d0b35..553f4d5 100644 --- a/app/src/main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt +++ b/app/src/main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt @@ -34,8 +34,12 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager import org.koin.core.component.KoinComponent import org.koin.core.component.inject import pe.aioo.openmoa.config.Config +import pe.aioo.openmoa.config.HangulInputMode +import pe.aioo.openmoa.config.OneHandMode import pe.aioo.openmoa.databinding.OpenMoaImeBinding import pe.aioo.openmoa.hangul.HangulAssembler +import pe.aioo.openmoa.settings.SettingsActivity +import pe.aioo.openmoa.settings.SettingsPreferences import pe.aioo.openmoa.view.keyboardview.* import pe.aioo.openmoa.view.keyboardview.qwerty.QuertyView import pe.aioo.openmoa.view.message.SpecialKey @@ -46,7 +50,7 @@ class OpenMoaIME : InputMethodService(), KoinComponent { private lateinit var binding: OpenMoaImeBinding private lateinit var broadcastReceiver: BroadcastReceiver - private lateinit var keyboardViews: Map + private lateinit var keyboardViews: MutableMap private val config: Config by inject() private val hangulAssembler = HangulAssembler() private var imeMode = IMEMode.IME_KO @@ -310,6 +314,13 @@ class OpenMoaIME : InputMethodService(), KoinComponent { setKeyboard(IMEMode.IME_EMOJI) } } + SpecialKey.OPEN_SETTINGS -> { + startActivity( + Intent(this@OpenMoaIME, SettingsActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + ) + } } } is String -> { @@ -414,7 +425,7 @@ class OpenMoaIME : InputMethodService(), KoinComponent { val arrowView = ArrowView(this) val phoneView = PhoneView(this) val emojiView = EmojiView(this) - keyboardViews = mapOf( + keyboardViews = mutableMapOf( IMEMode.IME_KO to OpenMoaView(this), IMEMode.IME_EN to QuertyView(this), IMEMode.IME_KO_PUNCTUATION to punctuationView, @@ -429,13 +440,24 @@ class OpenMoaIME : InputMethodService(), KoinComponent { ) val view = layoutInflater.inflate(R.layout.open_moa_ime, null) binding = OpenMoaImeBinding.bind(view) + applyKeyboardLayout() setKeyboard(imeMode) return view } + private fun refreshOpenMoaViewIfNeeded() { + val savedIsMoakey = SettingsPreferences.getHangulInputMode(this) == HangulInputMode.MOAKEY + val currentView = keyboardViews[IMEMode.IME_KO] as? OpenMoaView ?: return + if (currentView.isMoakeyMode != savedIsMoakey) { + keyboardViews[IMEMode.IME_KO] = OpenMoaView(this) + } + } + override fun onStartInputView(info: EditorInfo?, restarting: Boolean) { super.onStartInputView(info, restarting) finishComposing() + refreshOpenMoaViewIfNeeded() + applyKeyboardLayout() when ((info?.inputType ?: 0) and InputType.TYPE_MASK_CLASS) { InputType.TYPE_CLASS_NUMBER -> { setKeyboard( @@ -581,6 +603,28 @@ class OpenMoaIME : InputMethodService(), KoinComponent { .build() } + private fun applyKeyboardLayout() { + val oneHandMode = SettingsPreferences.getOneHandMode(this) + val displayWidth = resources.displayMetrics.widthPixels + val keyboardWidth = if (oneHandMode.isReduced) { + (displayWidth * 0.80f).toInt() + } else { + android.view.ViewGroup.LayoutParams.MATCH_PARENT + } + val params = android.widget.FrameLayout.LayoutParams(keyboardWidth, calculateKeyboardHeight()).apply { + gravity = oneHandMode.gravity + } + binding.keyboardFrameLayout.layoutParams = params + } + + private fun calculateKeyboardHeight(): Int { + val displayHeight = resources.displayMetrics.heightPixels + val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + val baseScale = if (isLandscape) 0.50f else 0.35f + val heightScale = SettingsPreferences.getKeypadHeight(this).heightScale + return (displayHeight * baseScale * heightScale).toInt() + } + private fun getHeight(): Int { return toDp(32) } diff --git a/app/src/main/kotlin/pe/aioo/openmoa/config/HangulInputMode.kt b/app/src/main/kotlin/pe/aioo/openmoa/config/HangulInputMode.kt new file mode 100644 index 0000000..421210b --- /dev/null +++ b/app/src/main/kotlin/pe/aioo/openmoa/config/HangulInputMode.kt @@ -0,0 +1,13 @@ +package pe.aioo.openmoa.config + +import pe.aioo.openmoa.R + +enum class HangulInputMode(val labelResId: Int) { + MOAKEY(R.string.settings_input_mode_moakey), + TWO_HAND_MOAKEY(R.string.settings_input_mode_two_hand_moakey); + + companion object { + fun fromString(value: String?): HangulInputMode = + values().find { it.name == value } ?: MOAKEY + } +} diff --git a/app/src/main/kotlin/pe/aioo/openmoa/config/KeypadHeight.kt b/app/src/main/kotlin/pe/aioo/openmoa/config/KeypadHeight.kt new file mode 100644 index 0000000..e8c4867 --- /dev/null +++ b/app/src/main/kotlin/pe/aioo/openmoa/config/KeypadHeight.kt @@ -0,0 +1,14 @@ +package pe.aioo.openmoa.config + +import pe.aioo.openmoa.R + +enum class KeypadHeight(val labelResId: Int, val heightScale: Float) { + NORMAL(R.string.settings_keypad_height_normal, 1.0f), + LOW(R.string.settings_keypad_height_low, 0.85f), + VERY_LOW(R.string.settings_keypad_height_very_low, 0.70f); + + companion object { + fun fromString(value: String?): KeypadHeight = + values().find { it.name == value } ?: NORMAL + } +} diff --git a/app/src/main/kotlin/pe/aioo/openmoa/config/OneHandMode.kt b/app/src/main/kotlin/pe/aioo/openmoa/config/OneHandMode.kt new file mode 100644 index 0000000..be3cbdd --- /dev/null +++ b/app/src/main/kotlin/pe/aioo/openmoa/config/OneHandMode.kt @@ -0,0 +1,18 @@ +package pe.aioo.openmoa.config + +import android.view.Gravity +import pe.aioo.openmoa.R + +enum class OneHandMode(val labelResId: Int, val gravity: Int) { + NONE(R.string.settings_one_hand_mode_none, Gravity.START), + LEFT(R.string.settings_one_hand_mode_left, Gravity.START), + RIGHT(R.string.settings_one_hand_mode_right, Gravity.END), + CENTER(R.string.settings_one_hand_mode_center, Gravity.CENTER_HORIZONTAL); + + val isReduced: Boolean get() = this != NONE + + companion object { + fun fromString(value: String?): OneHandMode = + values().find { it.name == value } ?: NONE + } +} diff --git a/app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsActivity.kt b/app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsActivity.kt new file mode 100644 index 0000000..6abff53 --- /dev/null +++ b/app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsActivity.kt @@ -0,0 +1,95 @@ +package pe.aioo.openmoa.settings + +import android.os.Bundle +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import pe.aioo.openmoa.R +import pe.aioo.openmoa.config.HangulInputMode +import pe.aioo.openmoa.config.KeypadHeight +import pe.aioo.openmoa.config.OneHandMode +import pe.aioo.openmoa.databinding.ActivitySettingsBinding + +class SettingsActivity : AppCompatActivity() { + + private lateinit var binding: ActivitySettingsBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivitySettingsBinding.inflate(layoutInflater) + setContentView(binding.root) + setupViews() + } + + private fun setupViews() { + updateInputModeDisplay() + updateKeypadHeightDisplay() + updateOneHandModeDisplay() + + binding.hangulInputModeItem.setOnClickListener { showInputModeDialog() } + binding.keypadHeightItem.setOnClickListener { showKeypadHeightDialog() } + binding.oneHandModeItem.setOnClickListener { showOneHandModeDialog() } + } + + private fun updateInputModeDisplay() { + binding.hangulInputModeValue.text = + getString(SettingsPreferences.getHangulInputMode(this).labelResId) + } + + private fun updateKeypadHeightDisplay() { + binding.keypadHeightValue.text = + getString(SettingsPreferences.getKeypadHeight(this).labelResId) + } + + private fun updateOneHandModeDisplay() { + binding.oneHandModeValue.text = + getString(SettingsPreferences.getOneHandMode(this).labelResId) + } + + private fun showInputModeDialog() { + val modes = HangulInputMode.values() + val labels = modes.map { getString(it.labelResId) }.toTypedArray() + val currentIndex = modes.indexOf(SettingsPreferences.getHangulInputMode(this)) + + AlertDialog.Builder(this) + .setTitle(R.string.settings_hangul_input_mode_title) + .setSingleChoiceItems(labels, currentIndex) { dialog, which -> + SettingsPreferences.save(this, SettingsPreferences.KEY_HANGUL_INPUT_MODE, modes[which].name) + updateInputModeDisplay() + dialog.dismiss() + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun showKeypadHeightDialog() { + val options = KeypadHeight.values() + val labels = options.map { getString(it.labelResId) }.toTypedArray() + val currentIndex = options.indexOf(SettingsPreferences.getKeypadHeight(this)) + + AlertDialog.Builder(this) + .setTitle(R.string.settings_keypad_height_title) + .setSingleChoiceItems(labels, currentIndex) { dialog, which -> + SettingsPreferences.save(this, SettingsPreferences.KEY_KEYPAD_HEIGHT, options[which].name) + updateKeypadHeightDisplay() + dialog.dismiss() + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun showOneHandModeDialog() { + val options = OneHandMode.values() + val labels = options.map { getString(it.labelResId) }.toTypedArray() + val currentIndex = options.indexOf(SettingsPreferences.getOneHandMode(this)) + + AlertDialog.Builder(this) + .setTitle(R.string.settings_one_hand_mode_title) + .setSingleChoiceItems(labels, currentIndex) { dialog, which -> + SettingsPreferences.save(this, SettingsPreferences.KEY_ONE_HAND_MODE, options[which].name) + updateOneHandModeDisplay() + dialog.dismiss() + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } +} diff --git a/app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsPreferences.kt b/app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsPreferences.kt new file mode 100644 index 0000000..a768bbc --- /dev/null +++ b/app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsPreferences.kt @@ -0,0 +1,39 @@ +package pe.aioo.openmoa.settings + +import android.content.Context +import pe.aioo.openmoa.config.HangulInputMode +import pe.aioo.openmoa.config.KeypadHeight +import pe.aioo.openmoa.config.OneHandMode + +object SettingsPreferences { + + const val PREFS_NAME = "openmoa_settings" + const val KEY_HANGUL_INPUT_MODE = "hangul_input_mode" + const val KEY_KEYPAD_HEIGHT = "keypad_height" + const val KEY_ONE_HAND_MODE = "one_hand_mode" + + fun getHangulInputMode(context: Context): HangulInputMode { + val value = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getString(KEY_HANGUL_INPUT_MODE, null) + return HangulInputMode.fromString(value) + } + + fun getKeypadHeight(context: Context): KeypadHeight { + val value = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getString(KEY_KEYPAD_HEIGHT, null) + return KeypadHeight.fromString(value) + } + + fun getOneHandMode(context: Context): OneHandMode { + val value = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getString(KEY_ONE_HAND_MODE, null) + return OneHandMode.fromString(value) + } + + fun save(context: Context, key: String, value: String) { + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .putString(key, value) + .apply() + } +} diff --git a/app/src/main/kotlin/pe/aioo/openmoa/view/KeyboardFrameLayout.kt b/app/src/main/kotlin/pe/aioo/openmoa/view/KeyboardFrameLayout.kt index c7f940f..6c475b2 100644 --- a/app/src/main/kotlin/pe/aioo/openmoa/view/KeyboardFrameLayout.kt +++ b/app/src/main/kotlin/pe/aioo/openmoa/view/KeyboardFrameLayout.kt @@ -38,7 +38,7 @@ class KeyboardFrameLayout : FrameLayout { } } binding.keyboardLayout.removeAllViews() - binding.keyboardLayout.addView(view) + binding.keyboardLayout.addView(view, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)) } } \ No newline at end of file diff --git a/app/src/main/kotlin/pe/aioo/openmoa/view/keyboardview/OpenMoaView.kt b/app/src/main/kotlin/pe/aioo/openmoa/view/keyboardview/OpenMoaView.kt index 6938fb3..0e38c6a 100644 --- a/app/src/main/kotlin/pe/aioo/openmoa/view/keyboardview/OpenMoaView.kt +++ b/app/src/main/kotlin/pe/aioo/openmoa/view/keyboardview/OpenMoaView.kt @@ -14,10 +14,14 @@ import org.koin.core.component.inject import pe.aioo.openmoa.OpenMoaIME import pe.aioo.openmoa.R import pe.aioo.openmoa.config.Config +import pe.aioo.openmoa.config.HangulInputMode +import pe.aioo.openmoa.settings.SettingsPreferences import pe.aioo.openmoa.view.message.SpecialKey import pe.aioo.openmoa.databinding.OpenMoaViewBinding +import pe.aioo.openmoa.databinding.OpenMoaViewMoakeyBinding import pe.aioo.openmoa.view.keytouchlistener.CrossKeyTouchListener import pe.aioo.openmoa.view.keytouchlistener.JaumKeyTouchListener +import pe.aioo.openmoa.view.keytouchlistener.LanguageKeyTouchListener import pe.aioo.openmoa.view.keytouchlistener.RepeatKeyTouchListener import pe.aioo.openmoa.view.keytouchlistener.SimpleKeyTouchListener import pe.aioo.openmoa.view.message.SpecialKeyMessage @@ -42,7 +46,10 @@ class OpenMoaView : ConstraintLayout, KoinComponent { } private val broadcastManager = LocalBroadcastManager.getInstance(context) - private lateinit var binding: OpenMoaViewBinding + internal var isMoakeyMode = false + private set + private var twoHandBinding: OpenMoaViewBinding? = null + private var moakeyBinding: OpenMoaViewMoakeyBinding? = null private var touchedMoeum: String? = null private val backgrounds = listOf( ContextCompat.getDrawable(context, R.drawable.key_background_pressed), @@ -50,14 +57,23 @@ class OpenMoaView : ConstraintLayout, KoinComponent { ) private fun init() { - inflate(context, R.layout.open_moa_view, this) - binding = OpenMoaViewBinding.bind(this) - setOnTouchListeners() + val mode = SettingsPreferences.getHangulInputMode(context) + isMoakeyMode = mode == HangulInputMode.MOAKEY + if (isMoakeyMode) { + inflate(context, R.layout.open_moa_view_moakey, this) + moakeyBinding = OpenMoaViewMoakeyBinding.bind(this) + setMoakeyTouchListeners() + } else { + inflate(context, R.layout.open_moa_view, this) + twoHandBinding = OpenMoaViewBinding.bind(this) + setTwoHandTouchListeners() + } } @SuppressLint("ClickableViewAccessibility") - private fun setOnTouchListeners() { - binding.apply { + private fun setTwoHandTouchListeners() { + val b = twoHandBinding ?: return + b.apply { tildeKey.setOnTouchListener(SimpleKeyTouchListener(context, StringKeyMessage("~"))) ssangbieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅃ")) ssangjieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅉ")) @@ -91,9 +107,7 @@ class OpenMoaView : ConstraintLayout, KoinComponent { tieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅌ")) chieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅊ")) pieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅍ")) - languageKey.setOnTouchListener( - SimpleKeyTouchListener(context, SpecialKeyMessage(SpecialKey.LANGUAGE)) - ) + languageKey.setOnTouchListener(LanguageKeyTouchListener(context)) hanjaNumberPunctuationKey.setOnTouchListener( SimpleKeyTouchListener( context, SpecialKeyMessage(SpecialKey.HANJA_NUMBER_PUNCTUATION) @@ -117,86 +131,157 @@ class OpenMoaView : ConstraintLayout, KoinComponent { } } + @SuppressLint("ClickableViewAccessibility") + private fun setMoakeyTouchListeners() { + val b = moakeyBinding ?: return + b.apply { + tildeKey.setOnTouchListener(SimpleKeyTouchListener(context, StringKeyMessage("~"))) + ssangbieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅃ")) + ssangjieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅉ")) + ssangdigeutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄸ")) + ssanggiyeokKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄲ")) + ssangsiotKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅆ")) + exclamationKey.setOnTouchListener( + SimpleKeyTouchListener(context, StringKeyMessage("!")) + ) + caretKey.setOnTouchListener(SimpleKeyTouchListener(context, StringKeyMessage("^"))) + bieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅂ")) + jieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅈ")) + digeutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄷ")) + giyeokKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄱ")) + siotKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅅ")) + questionKey.setOnTouchListener( + SimpleKeyTouchListener(context, StringKeyMessage("?")) + ) + semicolonKey.setOnTouchListener( + SimpleKeyTouchListener(context, StringKeyMessage(";")) + ) + mieumKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅁ")) + nieunKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄴ")) + ieungKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅇ")) + rieulKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄹ")) + hieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅎ")) + dotKey.setOnTouchListener(SimpleKeyTouchListener(context, StringKeyMessage("."))) + asteriskKey.setOnTouchListener( + SimpleKeyTouchListener(context, StringKeyMessage("*")) + ) + kieukKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅋ")) + tieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅌ")) + chieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅊ")) + pieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅍ")) + backspaceKey.setOnTouchListener( + RepeatKeyTouchListener(context, SpecialKeyMessage(SpecialKey.BACKSPACE)) + ) + emojiKey.setOnTouchListener( + SimpleKeyTouchListener(context, SpecialKeyMessage(SpecialKey.EMOJI)) + ) + languageKey.setOnTouchListener(LanguageKeyTouchListener(context)) + hanjaNumberPunctuationKey.setOnTouchListener( + SimpleKeyTouchListener( + context, SpecialKeyMessage(SpecialKey.HANJA_NUMBER_PUNCTUATION) + ) + ) + spaceKey.setOnTouchListener(SimpleKeyTouchListener(context, StringKeyMessage(" "))) + moeumKey.setOnTouchListener( + CrossKeyTouchListener( + context, + listOf( + StringKeyMessage("ᆢ"), + StringKeyMessage("ㅡ"), + StringKeyMessage("ㆍ"), + StringKeyMessage("ㅣ"), + ), + ) + ) + enterKey.setOnTouchListener( + SimpleKeyTouchListener(context, SpecialKeyMessage(SpecialKey.ENTER)) + ) + } + } + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { - touchedMoeum.let { moeum -> - when (ev.action) { - MotionEvent.ACTION_DOWN, - MotionEvent.ACTION_MOVE -> { - if (ev.action == MotionEvent.ACTION_DOWN || - (ev.action == MotionEvent.ACTION_MOVE && touchedMoeum != null) - ) { - binding.iKey.apply { - if (ev.x in x..x + width && ev.y in y..y + height) { - if (moeum != "ㅣ") { - background = backgrounds[0] - binding.euKey.background = backgrounds[1] - binding.araeaKey.background = backgrounds[1] - if (config.hapticFeedback) { - performHapticFeedback( - HapticFeedbackConstants.KEYBOARD_PRESS - ) - } - if (moeum != null) { - sendKeyMessage(StringKeyMessage(moeum)) + if (!isMoakeyMode) { + val b = twoHandBinding ?: return super.dispatchTouchEvent(ev) + touchedMoeum.let { moeum -> + when (ev.action) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_MOVE -> { + if (ev.action == MotionEvent.ACTION_DOWN || + (ev.action == MotionEvent.ACTION_MOVE && touchedMoeum != null) + ) { + b.iKey.apply { + if (ev.x in x..x + width && ev.y in y..y + height) { + if (moeum != "ㅣ") { + background = backgrounds[0] + b.euKey.background = backgrounds[1] + b.araeaKey.background = backgrounds[1] + if (config.hapticFeedback) { + performHapticFeedback( + HapticFeedbackConstants.KEYBOARD_PRESS + ) + } + if (moeum != null) { + sendKeyMessage(StringKeyMessage(moeum)) + } } + touchedMoeum = "ㅣ" + return true } - touchedMoeum = "ㅣ" - return true } - } - binding.euKey.apply { - if (ev.x in x..x + width && ev.y in y..y + height) { - if (moeum != "ㅡ") { - background = backgrounds[0] - binding.iKey.background = backgrounds[1] - binding.araeaKey.background = backgrounds[1] - if (config.hapticFeedback) { - performHapticFeedback( - HapticFeedbackConstants.KEYBOARD_PRESS - ) - } - if (moeum != null) { - sendKeyMessage(StringKeyMessage(moeum)) + b.euKey.apply { + if (ev.x in x..x + width && ev.y in y..y + height) { + if (moeum != "ㅡ") { + background = backgrounds[0] + b.iKey.background = backgrounds[1] + b.araeaKey.background = backgrounds[1] + if (config.hapticFeedback) { + performHapticFeedback( + HapticFeedbackConstants.KEYBOARD_PRESS + ) + } + if (moeum != null) { + sendKeyMessage(StringKeyMessage(moeum)) + } } + touchedMoeum = "ㅡ" + return true } - touchedMoeum = "ㅡ" - return true } - } - binding.araeaKey.apply { - if (ev.x in x..x + width && ev.y in y..y + height) { - if (moeum != "ㆍ") { - background = backgrounds[0] - binding.iKey.background = backgrounds[1] - binding.euKey.background = backgrounds[1] - if (config.hapticFeedback) { - performHapticFeedback( - HapticFeedbackConstants.KEYBOARD_PRESS - ) - } - if (moeum != null) { - sendKeyMessage(StringKeyMessage(moeum)) + b.araeaKey.apply { + if (ev.x in x..x + width && ev.y in y..y + height) { + if (moeum != "ㆍ") { + background = backgrounds[0] + b.iKey.background = backgrounds[1] + b.euKey.background = backgrounds[1] + if (config.hapticFeedback) { + performHapticFeedback( + HapticFeedbackConstants.KEYBOARD_PRESS + ) + } + if (moeum != null) { + sendKeyMessage(StringKeyMessage(moeum)) + } } + touchedMoeum = "ㆍ" + return true } - touchedMoeum = "ㆍ" - return true } } + Unit } - Unit - } - MotionEvent.ACTION_UP -> { - if (moeum != null) { - binding.iKey.background = backgrounds[1] - binding.euKey.background = backgrounds[1] - binding.araeaKey.background = backgrounds[1] - sendKeyMessage(StringKeyMessage(moeum)) - touchedMoeum = null - return true + MotionEvent.ACTION_UP -> { + if (moeum != null) { + b.iKey.background = backgrounds[1] + b.euKey.background = backgrounds[1] + b.araeaKey.background = backgrounds[1] + sendKeyMessage(StringKeyMessage(moeum)) + touchedMoeum = null + return true + } + Unit } - Unit + else -> Unit } - else -> Unit } } return super.dispatchTouchEvent(ev) @@ -210,4 +295,4 @@ class OpenMoaView : ConstraintLayout, KoinComponent { ) } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/pe/aioo/openmoa/view/keytouchlistener/LanguageKeyTouchListener.kt b/app/src/main/kotlin/pe/aioo/openmoa/view/keytouchlistener/LanguageKeyTouchListener.kt new file mode 100644 index 0000000..5ce7c88 --- /dev/null +++ b/app/src/main/kotlin/pe/aioo/openmoa/view/keytouchlistener/LanguageKeyTouchListener.kt @@ -0,0 +1,57 @@ +package pe.aioo.openmoa.view.keytouchlistener + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.view.MotionEvent +import android.view.View +import pe.aioo.openmoa.view.message.SpecialKey +import pe.aioo.openmoa.view.message.SpecialKeyMessage +import java.util.Timer + +class LanguageKeyTouchListener(context: Context) : BaseKeyTouchListener(context) { + + @Volatile + private var longPressTriggered = false + private var timer: Timer? = null + private val mainHandler = Handler(Looper.getMainLooper()) + + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(view: View, motionEvent: MotionEvent): Boolean { + when (motionEvent.action) { + MotionEvent.ACTION_DOWN -> { + longPressTriggered = false + var elapsed = 0L + timer = kotlin.concurrent.timer(period = config.longPressRepeatTime) { + elapsed += config.longPressRepeatTime + if (elapsed >= LONG_PRESS_THRESHOLD_MS) { + longPressTriggered = true + cancel() + mainHandler.post { + sendKeyMessage(SpecialKeyMessage(SpecialKey.OPEN_SETTINGS)) + } + } + } + } + MotionEvent.ACTION_UP -> { + timer?.cancel() + timer = null + if (!longPressTriggered) { + sendKeyMessage(SpecialKeyMessage(SpecialKey.LANGUAGE)) + } + longPressTriggered = false + } + MotionEvent.ACTION_CANCEL -> { + timer?.cancel() + timer = null + longPressTriggered = false + } + } + return super.onTouch(view, motionEvent) + } + + companion object { + private const val LONG_PRESS_THRESHOLD_MS = 300L + } +} diff --git a/app/src/main/kotlin/pe/aioo/openmoa/view/message/SpecialKey.kt b/app/src/main/kotlin/pe/aioo/openmoa/view/message/SpecialKey.kt index 5e6bc87..ee655c1 100644 --- a/app/src/main/kotlin/pe/aioo/openmoa/view/message/SpecialKey.kt +++ b/app/src/main/kotlin/pe/aioo/openmoa/view/message/SpecialKey.kt @@ -18,6 +18,7 @@ enum class SpecialKey { HANJA_NUMBER_PUNCTUATION, HOME, LANGUAGE, + OPEN_SETTINGS, PASTE, SELECT_ALL, SELECT_ARROW_UP, diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml new file mode 100644 index 0000000..93d624d --- /dev/null +++ b/app/src/main/res/layout/activity_settings.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/open_moa_ime.xml b/app/src/main/res/layout/open_moa_ime.xml index 3147a12..8c9d751 100644 --- a/app/src/main/res/layout/open_moa_ime.xml +++ b/app/src/main/res/layout/open_moa_ime.xml @@ -46,9 +46,16 @@ - + android:layout_height="wrap_content"> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/open_moa_view.xml b/app/src/main/res/layout/open_moa_view.xml index 4e10a19..733b9de 100644 --- a/app/src/main/res/layout/open_moa_view.xml +++ b/app/src/main/res/layout/open_moa_view.xml @@ -337,11 +337,15 @@ - - - + + + + + + + + + app:layout_constraintRight_toLeftOf="@+id/spaceKey"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8ceb60c..0de8ab0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -27,7 +27,8 @@ 5 4 - 漢\n12/기 + 12/기 + 처음으로 @@ -68,4 +69,26 @@ 0 + ! + ? + ㅣ·ㅡ + + 설정 + 오픈모아 통합키보드 설정 + 한글설정 + 한글 입력 방식 선택 + 모아키 + 양손 모아키 + + 키보드 옵션 + 키패드 높이 조절 + 사용안함(기본) + 낮게 + 아주 낮게 + 한손 입력모드 설정 + 사용안함(기본) + 왼손 사용 + 오른손 사용 + 가운데 정렬 + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..5b0d397 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/gradle.properties b/gradle.properties index a2e90d8..543c5df 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,5 +21,5 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true -android.defaults.buildfeatures.buildconfig=true +# android.defaults.buildfeatures.buildconfig=true (removed: deprecated in AGP 10.0) android.nonFinalResIds=false \ No newline at end of file From 6a0ea6b3b822f3d7746c20dd432d1d0b321dd969 Mon Sep 17 00:00:00 2001 From: Songbum Bae Date: Mon, 20 Apr 2026 18:48:31 +0900 Subject: [PATCH 02/20] =?UTF-8?q?feat:=20=EC=9E=90=EC=9D=8C=20=ED=82=A4=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EC=8B=9C=20=EA=B8=80=EC=9E=90=20=ED=94=84?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=ED=8C=9D=EC=97=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 자음 키 터치 시 키 위쪽에 반투명 팝업으로 입력될 글자 표시 - 드래그 중 조합 글자 실시간 업데이트 (예: ㅇ → 오른쪽 스와이프 → 아) - PopupWindow 사용으로 SYSTEM_ALERT_WINDOW 권한 불필요 - 설정 화면에서 키 미리보기 ON/OFF 토글 지원 --- .../kotlin/pe/aioo/openmoa/config/Config.kt | 1 + .../openmoa/hangul/HangulPreviewComposer.kt | 17 ++++ .../pe/aioo/openmoa/module/configModule.kt | 8 +- .../aioo/openmoa/settings/SettingsActivity.kt | 12 +++ .../openmoa/settings/SettingsPreferences.kt | 13 +++ .../openmoa/view/keyboardview/OpenMoaView.kt | 84 ++++++++++--------- .../keytouchlistener/JaumKeyTouchListener.kt | 10 +++ .../view/preview/KeyPreviewController.kt | 24 ++++++ .../openmoa/view/preview/KeyPreviewPopup.kt | 53 ++++++++++++ .../res/drawable/key_preview_background.xml | 9 ++ app/src/main/res/layout/activity_settings.xml | 28 +++++++ app/src/main/res/layout/key_preview_popup.xml | 14 ++++ app/src/main/res/values/strings.xml | 1 + 13 files changed, 235 insertions(+), 39 deletions(-) create mode 100644 app/src/main/kotlin/pe/aioo/openmoa/hangul/HangulPreviewComposer.kt create mode 100644 app/src/main/kotlin/pe/aioo/openmoa/view/preview/KeyPreviewController.kt create mode 100644 app/src/main/kotlin/pe/aioo/openmoa/view/preview/KeyPreviewPopup.kt create mode 100644 app/src/main/res/drawable/key_preview_background.xml create mode 100644 app/src/main/res/layout/key_preview_popup.xml diff --git a/app/src/main/kotlin/pe/aioo/openmoa/config/Config.kt b/app/src/main/kotlin/pe/aioo/openmoa/config/Config.kt index e04ef11..71a0bed 100644 --- a/app/src/main/kotlin/pe/aioo/openmoa/config/Config.kt +++ b/app/src/main/kotlin/pe/aioo/openmoa/config/Config.kt @@ -6,4 +6,5 @@ data class Config ( val gestureThreshold: Float = 50f, val hapticFeedback: Boolean = true, val maxSuggestionCount: Int = 10, + val keyPreviewEnabled: Boolean = true, ) \ No newline at end of file diff --git a/app/src/main/kotlin/pe/aioo/openmoa/hangul/HangulPreviewComposer.kt b/app/src/main/kotlin/pe/aioo/openmoa/hangul/HangulPreviewComposer.kt new file mode 100644 index 0000000..afeabb1 --- /dev/null +++ b/app/src/main/kotlin/pe/aioo/openmoa/hangul/HangulPreviewComposer.kt @@ -0,0 +1,17 @@ +package pe.aioo.openmoa.hangul + +import com.github.kimkevin.hangulparser.HangulParser +import com.github.kimkevin.hangulparser.HangulParserException + +object HangulPreviewComposer { + + fun compose(jaum: String, moeum: String?): String { + moeum ?: return jaum + return try { + HangulParser.assemble(listOf(jaum, moeum)) + } catch (_: HangulParserException) { + jaum + } + } + +} diff --git a/app/src/main/kotlin/pe/aioo/openmoa/module/configModule.kt b/app/src/main/kotlin/pe/aioo/openmoa/module/configModule.kt index d1e9d68..92bec64 100644 --- a/app/src/main/kotlin/pe/aioo/openmoa/module/configModule.kt +++ b/app/src/main/kotlin/pe/aioo/openmoa/module/configModule.kt @@ -1,8 +1,14 @@ package pe.aioo.openmoa.module +import org.koin.android.ext.koin.androidContext import org.koin.dsl.module import pe.aioo.openmoa.config.Config +import pe.aioo.openmoa.settings.SettingsPreferences val configModule = module { - single { Config() } + single { + Config( + keyPreviewEnabled = SettingsPreferences.getKeyPreviewEnabled(androidContext()) + ) + } } \ No newline at end of file diff --git a/app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsActivity.kt b/app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsActivity.kt index 6abff53..8566d21 100644 --- a/app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsActivity.kt +++ b/app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsActivity.kt @@ -24,10 +24,12 @@ class SettingsActivity : AppCompatActivity() { updateInputModeDisplay() updateKeypadHeightDisplay() updateOneHandModeDisplay() + updateKeyPreviewDisplay() binding.hangulInputModeItem.setOnClickListener { showInputModeDialog() } binding.keypadHeightItem.setOnClickListener { showKeypadHeightDialog() } binding.oneHandModeItem.setOnClickListener { showOneHandModeDialog() } + binding.keyPreviewItem.setOnClickListener { toggleKeyPreview() } } private fun updateInputModeDisplay() { @@ -45,6 +47,16 @@ class SettingsActivity : AppCompatActivity() { getString(SettingsPreferences.getOneHandMode(this).labelResId) } + private fun updateKeyPreviewDisplay() { + binding.keyPreviewSwitch.isChecked = SettingsPreferences.getKeyPreviewEnabled(this) + } + + private fun toggleKeyPreview() { + val newValue = !SettingsPreferences.getKeyPreviewEnabled(this) + SettingsPreferences.setKeyPreviewEnabled(this, newValue) + binding.keyPreviewSwitch.isChecked = newValue + } + private fun showInputModeDialog() { val modes = HangulInputMode.values() val labels = modes.map { getString(it.labelResId) }.toTypedArray() diff --git a/app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsPreferences.kt b/app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsPreferences.kt index a768bbc..e94d198 100644 --- a/app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsPreferences.kt +++ b/app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsPreferences.kt @@ -11,6 +11,19 @@ object SettingsPreferences { const val KEY_HANGUL_INPUT_MODE = "hangul_input_mode" const val KEY_KEYPAD_HEIGHT = "keypad_height" const val KEY_ONE_HAND_MODE = "one_hand_mode" + const val KEY_KEY_PREVIEW = "key_preview_enabled" + + fun getKeyPreviewEnabled(context: Context): Boolean { + return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getBoolean(KEY_KEY_PREVIEW, true) + } + + fun setKeyPreviewEnabled(context: Context, enabled: Boolean) { + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .putBoolean(KEY_KEY_PREVIEW, enabled) + .apply() + } fun getHangulInputMode(context: Context): HangulInputMode { val value = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) diff --git a/app/src/main/kotlin/pe/aioo/openmoa/view/keyboardview/OpenMoaView.kt b/app/src/main/kotlin/pe/aioo/openmoa/view/keyboardview/OpenMoaView.kt index 0e38c6a..98083c2 100644 --- a/app/src/main/kotlin/pe/aioo/openmoa/view/keyboardview/OpenMoaView.kt +++ b/app/src/main/kotlin/pe/aioo/openmoa/view/keyboardview/OpenMoaView.kt @@ -26,6 +26,7 @@ import pe.aioo.openmoa.view.keytouchlistener.RepeatKeyTouchListener import pe.aioo.openmoa.view.keytouchlistener.SimpleKeyTouchListener import pe.aioo.openmoa.view.message.SpecialKeyMessage import pe.aioo.openmoa.view.message.StringKeyMessage +import pe.aioo.openmoa.view.preview.KeyPreviewController class OpenMoaView : ConstraintLayout, KoinComponent { @@ -55,8 +56,10 @@ class OpenMoaView : ConstraintLayout, KoinComponent { ContextCompat.getDrawable(context, R.drawable.key_background_pressed), ContextCompat.getDrawable(context, R.drawable.key_background), ) + private lateinit var previewController: KeyPreviewController private fun init() { + previewController = KeyPreviewController(config.keyPreviewEnabled) val mode = SettingsPreferences.getHangulInputMode(context) isMoakeyMode = mode == HangulInputMode.MOAKEY if (isMoakeyMode) { @@ -70,43 +73,48 @@ class OpenMoaView : ConstraintLayout, KoinComponent { } } + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + previewController.hide() + } + @SuppressLint("ClickableViewAccessibility") private fun setTwoHandTouchListeners() { val b = twoHandBinding ?: return b.apply { tildeKey.setOnTouchListener(SimpleKeyTouchListener(context, StringKeyMessage("~"))) - ssangbieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅃ")) - ssangjieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅉ")) - ssangdigeutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄸ")) - ssanggiyeokKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄲ")) - ssangsiotKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅆ")) + ssangbieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅃ", previewController)) + ssangjieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅉ", previewController)) + ssangdigeutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄸ", previewController)) + ssanggiyeokKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄲ", previewController)) + ssangsiotKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅆ", previewController)) emojiKey.setOnTouchListener( SimpleKeyTouchListener(context, SpecialKeyMessage(SpecialKey.EMOJI)) ) caretKey.setOnTouchListener(SimpleKeyTouchListener(context, StringKeyMessage("^"))) - bieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅂ")) - jieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅈ")) - digeutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄷ")) - giyeokKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄱ")) - siotKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅅ")) + bieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅂ", previewController)) + jieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅈ", previewController)) + digeutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄷ", previewController)) + giyeokKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄱ", previewController)) + siotKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅅ", previewController)) backspaceKey.setOnTouchListener( RepeatKeyTouchListener(context, SpecialKeyMessage(SpecialKey.BACKSPACE)) ) semicolonKey.setOnTouchListener( SimpleKeyTouchListener(context, StringKeyMessage(";")) ) - mieumKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅁ")) - nieunKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄴ")) - ieungKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅇ")) - rieulKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄹ")) - hieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅎ")) + mieumKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅁ", previewController)) + nieunKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄴ", previewController)) + ieungKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅇ", previewController)) + rieulKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄹ", previewController)) + hieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅎ", previewController)) asteriskKey.setOnTouchListener( SimpleKeyTouchListener(context, StringKeyMessage("*")) ) - kieukKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅋ")) - tieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅌ")) - chieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅊ")) - pieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅍ")) + kieukKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅋ", previewController)) + tieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅌ", previewController)) + chieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅊ", previewController)) + pieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅍ", previewController)) languageKey.setOnTouchListener(LanguageKeyTouchListener(context)) hanjaNumberPunctuationKey.setOnTouchListener( SimpleKeyTouchListener( @@ -136,39 +144,39 @@ class OpenMoaView : ConstraintLayout, KoinComponent { val b = moakeyBinding ?: return b.apply { tildeKey.setOnTouchListener(SimpleKeyTouchListener(context, StringKeyMessage("~"))) - ssangbieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅃ")) - ssangjieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅉ")) - ssangdigeutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄸ")) - ssanggiyeokKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄲ")) - ssangsiotKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅆ")) + ssangbieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅃ", previewController)) + ssangjieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅉ", previewController)) + ssangdigeutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄸ", previewController)) + ssanggiyeokKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄲ", previewController)) + ssangsiotKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅆ", previewController)) exclamationKey.setOnTouchListener( SimpleKeyTouchListener(context, StringKeyMessage("!")) ) caretKey.setOnTouchListener(SimpleKeyTouchListener(context, StringKeyMessage("^"))) - bieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅂ")) - jieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅈ")) - digeutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄷ")) - giyeokKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄱ")) - siotKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅅ")) + bieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅂ", previewController)) + jieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅈ", previewController)) + digeutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄷ", previewController)) + giyeokKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄱ", previewController)) + siotKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅅ", previewController)) questionKey.setOnTouchListener( SimpleKeyTouchListener(context, StringKeyMessage("?")) ) semicolonKey.setOnTouchListener( SimpleKeyTouchListener(context, StringKeyMessage(";")) ) - mieumKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅁ")) - nieunKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄴ")) - ieungKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅇ")) - rieulKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄹ")) - hieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅎ")) + mieumKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅁ", previewController)) + nieunKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄴ", previewController)) + ieungKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅇ", previewController)) + rieulKey.setOnTouchListener(JaumKeyTouchListener(context, "ㄹ", previewController)) + hieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅎ", previewController)) dotKey.setOnTouchListener(SimpleKeyTouchListener(context, StringKeyMessage("."))) asteriskKey.setOnTouchListener( SimpleKeyTouchListener(context, StringKeyMessage("*")) ) - kieukKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅋ")) - tieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅌ")) - chieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅊ")) - pieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅍ")) + kieukKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅋ", previewController)) + tieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅌ", previewController)) + chieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅊ", previewController)) + pieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅍ", previewController)) backspaceKey.setOnTouchListener( RepeatKeyTouchListener(context, SpecialKeyMessage(SpecialKey.BACKSPACE)) ) diff --git a/app/src/main/kotlin/pe/aioo/openmoa/view/keytouchlistener/JaumKeyTouchListener.kt b/app/src/main/kotlin/pe/aioo/openmoa/view/keytouchlistener/JaumKeyTouchListener.kt index 1b7c946..4724775 100644 --- a/app/src/main/kotlin/pe/aioo/openmoa/view/keytouchlistener/JaumKeyTouchListener.kt +++ b/app/src/main/kotlin/pe/aioo/openmoa/view/keytouchlistener/JaumKeyTouchListener.kt @@ -4,13 +4,16 @@ import android.annotation.SuppressLint import android.content.Context import android.view.MotionEvent import android.view.View +import pe.aioo.openmoa.hangul.HangulPreviewComposer import pe.aioo.openmoa.hangul.MoeumGestureProcessor import pe.aioo.openmoa.view.message.StringKeyMessage +import pe.aioo.openmoa.view.preview.KeyPreviewController import kotlin.math.* class JaumKeyTouchListener( context: Context, private val key: String, + private val previewController: KeyPreviewController? = null, ) : BaseKeyTouchListener(context) { private var startX: Float = 0f @@ -24,6 +27,7 @@ class JaumKeyTouchListener( startX = motionEvent.x startY = motionEvent.y moeumGestureProcessor.clear() + previewController?.show(view, key) } MotionEvent.ACTION_MOVE -> { val currentX = motionEvent.x @@ -46,14 +50,20 @@ class JaumKeyTouchListener( } else if (abs(degree) <= 179.999f) { moeumGestureProcessor.appendMoeum("ㅓ") } + val moeum = moeumGestureProcessor.resolveMoeumList() + previewController?.update(view, HangulPreviewComposer.compose(key, moeum)) } } MotionEvent.ACTION_UP -> { + previewController?.hide() sendKeyMessage(StringKeyMessage(key)) moeumGestureProcessor.resolveMoeumList()?.let { sendKeyMessage(StringKeyMessage(it)) } } + MotionEvent.ACTION_CANCEL -> { + previewController?.hide() + } } return super.onTouch(view, motionEvent) } diff --git a/app/src/main/kotlin/pe/aioo/openmoa/view/preview/KeyPreviewController.kt b/app/src/main/kotlin/pe/aioo/openmoa/view/preview/KeyPreviewController.kt new file mode 100644 index 0000000..38ac9ed --- /dev/null +++ b/app/src/main/kotlin/pe/aioo/openmoa/view/preview/KeyPreviewController.kt @@ -0,0 +1,24 @@ +package pe.aioo.openmoa.view.preview + +import android.view.View + +class KeyPreviewController(private val enabled: Boolean) { + + private var popup: KeyPreviewPopup? = null + + fun show(anchor: View, text: String) { + if (!enabled) return + val p = popup ?: KeyPreviewPopup(anchor.context).also { popup = it } + p.show(anchor, text) + } + + fun update(anchor: View, text: String) { + if (!enabled) return + popup?.update(anchor, text) + } + + fun hide() { + popup?.hide() + } + +} diff --git a/app/src/main/kotlin/pe/aioo/openmoa/view/preview/KeyPreviewPopup.kt b/app/src/main/kotlin/pe/aioo/openmoa/view/preview/KeyPreviewPopup.kt new file mode 100644 index 0000000..0122e00 --- /dev/null +++ b/app/src/main/kotlin/pe/aioo/openmoa/view/preview/KeyPreviewPopup.kt @@ -0,0 +1,53 @@ +package pe.aioo.openmoa.view.preview + +import android.content.Context +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.PopupWindow +import android.widget.TextView +import pe.aioo.openmoa.R + +class KeyPreviewPopup(context: Context) { + + private val popupView: View = LayoutInflater.from(context).inflate( + R.layout.key_preview_popup, null + ) + private val previewText: TextView = popupView.findViewById(R.id.previewText) + private val popup = PopupWindow( + popupView, + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + false + ).apply { + isTouchable = false + } + + fun show(anchor: View, text: String) { + if (!anchor.isAttachedToWindow) return + previewText.text = text + popupView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) + val xoff = anchor.width / 2 - popupView.measuredWidth / 2 + val yoff = -(anchor.height + popupView.measuredHeight) + if (popup.isShowing) { + popup.update(anchor, xoff, yoff, -1, -1) + } else { + popup.showAsDropDown(anchor, xoff, yoff, Gravity.NO_GRAVITY) + } + } + + fun update(anchor: View, text: String) { + if (!popup.isShowing) return + previewText.text = text + popupView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) + val xoff = anchor.width / 2 - popupView.measuredWidth / 2 + val yoff = -(anchor.height + popupView.measuredHeight) + popup.update(anchor, xoff, yoff, -1, -1) + } + + fun hide() { + if (popup.isShowing) popup.dismiss() + } + +} diff --git a/app/src/main/res/drawable/key_preview_background.xml b/app/src/main/res/drawable/key_preview_background.xml new file mode 100644 index 0000000..279866e --- /dev/null +++ b/app/src/main/res/drawable/key_preview_background.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 93d624d..8ebee7a 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -127,6 +127,34 @@ + + + + + + + + diff --git a/app/src/main/res/layout/key_preview_popup.xml b/app/src/main/res/layout/key_preview_popup.xml new file mode 100644 index 0000000..4b9f949 --- /dev/null +++ b/app/src/main/res/layout/key_preview_popup.xml @@ -0,0 +1,14 @@ + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0de8ab0..782fb6d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -90,5 +90,6 @@ 왼손 사용 오른손 사용 가운데 정렬 + 키 입력 미리보기 \ No newline at end of file From 64f43ef31c9faa7b122d1416e2fb71bf5e678c1f Mon Sep 17 00:00:00 2001 From: Songbum Bae Date: Mon, 20 Apr 2026 19:47:09 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat:=20=EC=83=81=EC=9A=A9=EA=B5=AC=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(=E3=85=8B/=E3=85=8C/=E3=85=8A/=E3=85=8D=20=ED=82=A4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 자음 키 오른쪽 위에 상용구 첫 글자 뱃지 표시 - 탭: 상용구 전체 텍스트 입력 - 드래그: 기존 자음+모음 제스처 동작 유지 - 롱키: 팝업 메뉴 표시 (상용구 내용 / 수정 / 취소, 스와이프 선택) - 상용구 내용 선택 시 텍스트 입력 - 수정 선택 시 QuickPhraseEditActivity 팝업 실행 - 상용구 키 터치 시 키 프리뷰 미표시 - QuickPhraseEditActivity: 스피너로 키 선택, EditText로 내용 편집 - SettingsActivity에 상용구 섹션 추가 - longPressThresholdTime은 Config 공통 설정값 사용 --- app/src/main/AndroidManifest.xml | 5 + .../main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt | 1 + .../openmoa/quickphrase/QuickPhraseKey.kt | 12 ++ .../quickphrase/QuickPhraseRepository.kt | 25 ++++ .../settings/QuickPhraseEditActivity.kt | 65 +++++++++ .../aioo/openmoa/settings/SettingsActivity.kt | 41 ++++++ .../openmoa/view/keyboardview/OpenMoaView.kt | 41 ++++-- .../keytouchlistener/JaumKeyTouchListener.kt | 86 ++++++++++-- .../view/preview/QuickPhraseMenuPopup.kt | 74 +++++++++++ .../res/layout/activity_quick_phrase_edit.xml | 91 +++++++++++++ app/src/main/res/layout/activity_settings.xml | 123 ++++++++++++++++++ app/src/main/res/layout/open_moa_view.xml | 62 +++++++++ .../main/res/layout/open_moa_view_moakey.xml | 62 +++++++++ .../res/layout/quick_phrase_menu_popup.xml | 58 +++++++++ app/src/main/res/values/strings.xml | 13 ++ app/src/main/res/values/themes.xml | 6 + 16 files changed, 745 insertions(+), 20 deletions(-) create mode 100644 app/src/main/kotlin/pe/aioo/openmoa/quickphrase/QuickPhraseKey.kt create mode 100644 app/src/main/kotlin/pe/aioo/openmoa/quickphrase/QuickPhraseRepository.kt create mode 100644 app/src/main/kotlin/pe/aioo/openmoa/settings/QuickPhraseEditActivity.kt create mode 100644 app/src/main/kotlin/pe/aioo/openmoa/view/preview/QuickPhraseMenuPopup.kt create mode 100644 app/src/main/res/layout/activity_quick_phrase_edit.xml create mode 100644 app/src/main/res/layout/quick_phrase_menu_popup.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7af4741..2c146b4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,6 +34,11 @@ android:exported="false" android:theme="@style/Theme.OpenMoa.Settings" /> + + \ No newline at end of file diff --git a/app/src/main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt b/app/src/main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt index 553f4d5..fa735c4 100644 --- a/app/src/main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt +++ b/app/src/main/kotlin/pe/aioo/openmoa/OpenMoaIME.kt @@ -457,6 +457,7 @@ class OpenMoaIME : InputMethodService(), KoinComponent { super.onStartInputView(info, restarting) finishComposing() refreshOpenMoaViewIfNeeded() + (keyboardViews[IMEMode.IME_KO] as? OpenMoaView)?.refreshQuickPhraseBadges() applyKeyboardLayout() when ((info?.inputType ?: 0) and InputType.TYPE_MASK_CLASS) { InputType.TYPE_CLASS_NUMBER -> { diff --git a/app/src/main/kotlin/pe/aioo/openmoa/quickphrase/QuickPhraseKey.kt b/app/src/main/kotlin/pe/aioo/openmoa/quickphrase/QuickPhraseKey.kt new file mode 100644 index 0000000..3fd04d7 --- /dev/null +++ b/app/src/main/kotlin/pe/aioo/openmoa/quickphrase/QuickPhraseKey.kt @@ -0,0 +1,12 @@ +package pe.aioo.openmoa.quickphrase + +enum class QuickPhraseKey( + val jaum: String, + val defaultPhrase: String, + val prefKey: String, +) { + KIEUK("ㅋ", "안녕하세요", "quick_phrase_kieuk"), + TIEUT("ㅌ", "감사합니다", "quick_phrase_tieut"), + CHIEUT("ㅊ", "어디세요?", "quick_phrase_chieut"), + PIEUP("ㅍ", "연락바랍니다", "quick_phrase_pieup"), +} diff --git a/app/src/main/kotlin/pe/aioo/openmoa/quickphrase/QuickPhraseRepository.kt b/app/src/main/kotlin/pe/aioo/openmoa/quickphrase/QuickPhraseRepository.kt new file mode 100644 index 0000000..1945982 --- /dev/null +++ b/app/src/main/kotlin/pe/aioo/openmoa/quickphrase/QuickPhraseRepository.kt @@ -0,0 +1,25 @@ +package pe.aioo.openmoa.quickphrase + +import android.content.Context +import pe.aioo.openmoa.settings.SettingsPreferences + +object QuickPhraseRepository { + + fun getPhrase(context: Context, key: QuickPhraseKey): String { + val value = context.getSharedPreferences(SettingsPreferences.PREFS_NAME, Context.MODE_PRIVATE) + .getString(key.prefKey, null) + return if (value.isNullOrEmpty()) key.defaultPhrase else value + } + + fun setPhrase(context: Context, key: QuickPhraseKey, phrase: String) { + context.getSharedPreferences(SettingsPreferences.PREFS_NAME, Context.MODE_PRIVATE) + .edit() + .putString(key.prefKey, phrase) + .apply() + } + + fun getFirstChar(context: Context, key: QuickPhraseKey): String { + val phrase = getPhrase(context, key) + return phrase.firstOrNull()?.toString() ?: "" + } +} diff --git a/app/src/main/kotlin/pe/aioo/openmoa/settings/QuickPhraseEditActivity.kt b/app/src/main/kotlin/pe/aioo/openmoa/settings/QuickPhraseEditActivity.kt new file mode 100644 index 0000000..b12b35b --- /dev/null +++ b/app/src/main/kotlin/pe/aioo/openmoa/settings/QuickPhraseEditActivity.kt @@ -0,0 +1,65 @@ +package pe.aioo.openmoa.settings + +import android.os.Bundle +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import androidx.appcompat.app.AppCompatActivity +import pe.aioo.openmoa.databinding.ActivityQuickPhraseEditBinding +import pe.aioo.openmoa.quickphrase.QuickPhraseKey +import pe.aioo.openmoa.quickphrase.QuickPhraseRepository + +class QuickPhraseEditActivity : AppCompatActivity() { + + companion object { + const val EXTRA_KEY = "extra_key" + } + + private lateinit var binding: ActivityQuickPhraseEditBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityQuickPhraseEditBinding.inflate(layoutInflater) + setContentView(binding.root) + + val initialKey = intent.getStringExtra(EXTRA_KEY) + ?.let { runCatching { QuickPhraseKey.valueOf(it) }.getOrNull() } + ?: QuickPhraseKey.KIEUK + + setupSpinner(initialKey) + setupButtons() + } + + private fun setupSpinner(initialKey: QuickPhraseKey) { + val keys = QuickPhraseKey.values() + val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, keys.map { it.jaum }) + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.keySpinner.adapter = adapter + binding.keySpinner.setSelection(keys.indexOf(initialKey)) + binding.keySpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { + val selectedKey = QuickPhraseKey.values()[position] + binding.contentEditText.setText(QuickPhraseRepository.getPhrase(this@QuickPhraseEditActivity, selectedKey)) + } + + override fun onNothingSelected(parent: AdapterView<*>) {} + } + } + + private fun setupButtons() { + binding.confirmButton.setOnClickListener { + val selectedKey = QuickPhraseKey.values()[binding.keySpinner.selectedItemPosition] + val content = binding.contentEditText.text.toString().trim() + if (content.isNotEmpty()) { + QuickPhraseRepository.setPhrase(this, selectedKey, content) + } + finish() + } + binding.cancelButton.setOnClickListener { finish() } + binding.resetButton.setOnClickListener { + val selectedKey = QuickPhraseKey.values()[binding.keySpinner.selectedItemPosition] + QuickPhraseRepository.setPhrase(this, selectedKey, selectedKey.defaultPhrase) + binding.contentEditText.setText(selectedKey.defaultPhrase) + } + } +} diff --git a/app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsActivity.kt b/app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsActivity.kt index 8566d21..3aa306d 100644 --- a/app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsActivity.kt +++ b/app/src/main/kotlin/pe/aioo/openmoa/settings/SettingsActivity.kt @@ -1,6 +1,7 @@ package pe.aioo.openmoa.settings import android.os.Bundle +import android.widget.EditText import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import pe.aioo.openmoa.R @@ -8,6 +9,8 @@ import pe.aioo.openmoa.config.HangulInputMode import pe.aioo.openmoa.config.KeypadHeight import pe.aioo.openmoa.config.OneHandMode import pe.aioo.openmoa.databinding.ActivitySettingsBinding +import pe.aioo.openmoa.quickphrase.QuickPhraseKey +import pe.aioo.openmoa.quickphrase.QuickPhraseRepository class SettingsActivity : AppCompatActivity() { @@ -25,11 +28,49 @@ class SettingsActivity : AppCompatActivity() { updateKeypadHeightDisplay() updateOneHandModeDisplay() updateKeyPreviewDisplay() + updateQuickPhraseDisplays() binding.hangulInputModeItem.setOnClickListener { showInputModeDialog() } binding.keypadHeightItem.setOnClickListener { showKeypadHeightDialog() } binding.oneHandModeItem.setOnClickListener { showOneHandModeDialog() } binding.keyPreviewItem.setOnClickListener { toggleKeyPreview() } + binding.quickPhraseKieukItem.setOnClickListener { showQuickPhraseEditDialog(QuickPhraseKey.KIEUK) } + binding.quickPhraseTieutItem.setOnClickListener { showQuickPhraseEditDialog(QuickPhraseKey.TIEUT) } + binding.quickPhraseChieutItem.setOnClickListener { showQuickPhraseEditDialog(QuickPhraseKey.CHIEUT) } + binding.quickPhrasePieupItem.setOnClickListener { showQuickPhraseEditDialog(QuickPhraseKey.PIEUP) } + } + + private fun updateQuickPhraseDisplays() { + binding.quickPhraseKieukValue.text = QuickPhraseRepository.getPhrase(this, QuickPhraseKey.KIEUK) + binding.quickPhraseTieutValue.text = QuickPhraseRepository.getPhrase(this, QuickPhraseKey.TIEUT) + binding.quickPhraseChieutValue.text = QuickPhraseRepository.getPhrase(this, QuickPhraseKey.CHIEUT) + binding.quickPhrasePieupValue.text = QuickPhraseRepository.getPhrase(this, QuickPhraseKey.PIEUP) + } + + private fun showQuickPhraseEditDialog(key: QuickPhraseKey) { + val editText = EditText(this).apply { + setText(QuickPhraseRepository.getPhrase(this@SettingsActivity, key)) + hint = getString(R.string.settings_quick_phrase_edit_hint) + setSingleLine(true) + } + AlertDialog.Builder(this) + .setTitle("${key.jaum} ${getString(R.string.settings_quick_phrase_edit_hint)}") + .setView(editText) + .setPositiveButton(android.R.string.ok) { _, _ -> + val input = editText.text.toString().trim() + if (input.isNotEmpty()) { + QuickPhraseRepository.setPhrase(this, key, input) + } else { + QuickPhraseRepository.setPhrase(this, key, key.defaultPhrase) + } + updateQuickPhraseDisplays() + } + .setNeutralButton(R.string.settings_quick_phrase_reset) { _, _ -> + QuickPhraseRepository.setPhrase(this, key, key.defaultPhrase) + updateQuickPhraseDisplays() + } + .setNegativeButton(android.R.string.cancel, null) + .show() } private fun updateInputModeDisplay() { diff --git a/app/src/main/kotlin/pe/aioo/openmoa/view/keyboardview/OpenMoaView.kt b/app/src/main/kotlin/pe/aioo/openmoa/view/keyboardview/OpenMoaView.kt index 98083c2..b8904e0 100644 --- a/app/src/main/kotlin/pe/aioo/openmoa/view/keyboardview/OpenMoaView.kt +++ b/app/src/main/kotlin/pe/aioo/openmoa/view/keyboardview/OpenMoaView.kt @@ -26,7 +26,10 @@ import pe.aioo.openmoa.view.keytouchlistener.RepeatKeyTouchListener import pe.aioo.openmoa.view.keytouchlistener.SimpleKeyTouchListener import pe.aioo.openmoa.view.message.SpecialKeyMessage import pe.aioo.openmoa.view.message.StringKeyMessage +import pe.aioo.openmoa.quickphrase.QuickPhraseKey +import pe.aioo.openmoa.quickphrase.QuickPhraseRepository import pe.aioo.openmoa.view.preview.KeyPreviewController +import pe.aioo.openmoa.view.preview.QuickPhraseMenuPopup class OpenMoaView : ConstraintLayout, KoinComponent { @@ -71,6 +74,26 @@ class OpenMoaView : ConstraintLayout, KoinComponent { twoHandBinding = OpenMoaViewBinding.bind(this) setTwoHandTouchListeners() } + updateQuickPhraseBadges() + } + + fun refreshQuickPhraseBadges() { + updateQuickPhraseBadges() + } + + private fun updateQuickPhraseBadges() { + twoHandBinding?.apply { + kieukBadge.text = QuickPhraseRepository.getFirstChar(context, QuickPhraseKey.KIEUK) + tieutBadge.text = QuickPhraseRepository.getFirstChar(context, QuickPhraseKey.TIEUT) + chieutBadge.text = QuickPhraseRepository.getFirstChar(context, QuickPhraseKey.CHIEUT) + pieupBadge.text = QuickPhraseRepository.getFirstChar(context, QuickPhraseKey.PIEUP) + } + moakeyBinding?.apply { + kieukBadge.text = QuickPhraseRepository.getFirstChar(context, QuickPhraseKey.KIEUK) + tieutBadge.text = QuickPhraseRepository.getFirstChar(context, QuickPhraseKey.TIEUT) + chieutBadge.text = QuickPhraseRepository.getFirstChar(context, QuickPhraseKey.CHIEUT) + pieupBadge.text = QuickPhraseRepository.getFirstChar(context, QuickPhraseKey.PIEUP) + } } override fun onDetachedFromWindow() { @@ -81,6 +104,7 @@ class OpenMoaView : ConstraintLayout, KoinComponent { @SuppressLint("ClickableViewAccessibility") private fun setTwoHandTouchListeners() { val b = twoHandBinding ?: return + val quickPhraseMenuPopup = QuickPhraseMenuPopup(context) b.apply { tildeKey.setOnTouchListener(SimpleKeyTouchListener(context, StringKeyMessage("~"))) ssangbieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅃ", previewController)) @@ -111,10 +135,10 @@ class OpenMoaView : ConstraintLayout, KoinComponent { asteriskKey.setOnTouchListener( SimpleKeyTouchListener(context, StringKeyMessage("*")) ) - kieukKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅋ", previewController)) - tieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅌ", previewController)) - chieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅊ", previewController)) - pieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅍ", previewController)) + kieukKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅋ", previewController, QuickPhraseKey.KIEUK, quickPhraseMenuPopup)) + tieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅌ", previewController, QuickPhraseKey.TIEUT, quickPhraseMenuPopup)) + chieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅊ", previewController, QuickPhraseKey.CHIEUT, quickPhraseMenuPopup)) + pieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅍ", previewController, QuickPhraseKey.PIEUP, quickPhraseMenuPopup)) languageKey.setOnTouchListener(LanguageKeyTouchListener(context)) hanjaNumberPunctuationKey.setOnTouchListener( SimpleKeyTouchListener( @@ -142,6 +166,7 @@ class OpenMoaView : ConstraintLayout, KoinComponent { @SuppressLint("ClickableViewAccessibility") private fun setMoakeyTouchListeners() { val b = moakeyBinding ?: return + val quickPhraseMenuPopup = QuickPhraseMenuPopup(context) b.apply { tildeKey.setOnTouchListener(SimpleKeyTouchListener(context, StringKeyMessage("~"))) ssangbieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅃ", previewController)) @@ -173,10 +198,10 @@ class OpenMoaView : ConstraintLayout, KoinComponent { asteriskKey.setOnTouchListener( SimpleKeyTouchListener(context, StringKeyMessage("*")) ) - kieukKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅋ", previewController)) - tieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅌ", previewController)) - chieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅊ", previewController)) - pieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅍ", previewController)) + kieukKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅋ", previewController, QuickPhraseKey.KIEUK, quickPhraseMenuPopup)) + tieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅌ", previewController, QuickPhraseKey.TIEUT, quickPhraseMenuPopup)) + chieutKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅊ", previewController, QuickPhraseKey.CHIEUT, quickPhraseMenuPopup)) + pieupKey.setOnTouchListener(JaumKeyTouchListener(context, "ㅍ", previewController, QuickPhraseKey.PIEUP, quickPhraseMenuPopup)) backspaceKey.setOnTouchListener( RepeatKeyTouchListener(context, SpecialKeyMessage(SpecialKey.BACKSPACE)) ) diff --git a/app/src/main/kotlin/pe/aioo/openmoa/view/keytouchlistener/JaumKeyTouchListener.kt b/app/src/main/kotlin/pe/aioo/openmoa/view/keytouchlistener/JaumKeyTouchListener.kt index 4724775..d81f3e4 100644 --- a/app/src/main/kotlin/pe/aioo/openmoa/view/keytouchlistener/JaumKeyTouchListener.kt +++ b/app/src/main/kotlin/pe/aioo/openmoa/view/keytouchlistener/JaumKeyTouchListener.kt @@ -2,40 +2,72 @@ package pe.aioo.openmoa.view.keytouchlistener import android.annotation.SuppressLint import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.Looper import android.view.MotionEvent import android.view.View import pe.aioo.openmoa.hangul.HangulPreviewComposer import pe.aioo.openmoa.hangul.MoeumGestureProcessor +import pe.aioo.openmoa.quickphrase.QuickPhraseKey +import pe.aioo.openmoa.quickphrase.QuickPhraseRepository +import pe.aioo.openmoa.settings.QuickPhraseEditActivity import pe.aioo.openmoa.view.message.StringKeyMessage import pe.aioo.openmoa.view.preview.KeyPreviewController +import pe.aioo.openmoa.view.preview.QuickPhraseMenuPopup import kotlin.math.* class JaumKeyTouchListener( - context: Context, + private val context: Context, private val key: String, private val previewController: KeyPreviewController? = null, + private val quickPhraseKey: QuickPhraseKey? = null, + private val quickPhraseMenuPopup: QuickPhraseMenuPopup? = null, ) : BaseKeyTouchListener(context) { - private var startX: Float = 0f - private var startY: Float = 0f + private var startX = 0f + private var startY = 0f + private var longPressStartX = 0f + private var hasMoved = false + private var isLongPressed = false private val moeumGestureProcessor = MoeumGestureProcessor() + private val handler = Handler(Looper.getMainLooper()) + private var anchorView: View? = null + + private val longPressRunnable = Runnable { + val view = anchorView ?: return@Runnable + val phraseKey = quickPhraseKey ?: return@Runnable + isLongPressed = true + previewController?.hide() + val phrase = QuickPhraseRepository.getPhrase(context, phraseKey) + quickPhraseMenuPopup?.show(view, phrase) + } @SuppressLint("ClickableViewAccessibility") override fun onTouch(view: View, motionEvent: MotionEvent): Boolean { when (motionEvent.action) { MotionEvent.ACTION_DOWN -> { + anchorView = view startX = motionEvent.x startY = motionEvent.y + longPressStartX = motionEvent.x + hasMoved = false + isLongPressed = false moeumGestureProcessor.clear() - previewController?.show(view, key) + if (quickPhraseKey == null) previewController?.show(view, key) + if (quickPhraseKey != null) { + handler.postDelayed(longPressRunnable, config.longPressThresholdTime) + } } MotionEvent.ACTION_MOVE -> { val currentX = motionEvent.x val currentY = motionEvent.y - val distance = sqrt( - (currentX - startX).pow(2) + (currentY - startY).pow(2) - ) + val distance = sqrt((currentX - startX).pow(2) + (currentY - startY).pow(2)) if (distance > config.gestureThreshold) { + if (!hasMoved) { + hasMoved = true + handler.removeCallbacks(longPressRunnable) + } val degree = (atan2(currentY - startY, currentX - startX) * 180f) / PI startX = currentX startY = currentY @@ -53,19 +85,49 @@ class JaumKeyTouchListener( val moeum = moeumGestureProcessor.resolveMoeumList() previewController?.update(view, HangulPreviewComposer.compose(key, moeum)) } + if (isLongPressed) { + val dx = motionEvent.x - longPressStartX + quickPhraseMenuPopup?.updateSelectionByDelta(dx, view.width) + } } MotionEvent.ACTION_UP -> { + handler.removeCallbacks(longPressRunnable) previewController?.hide() - sendKeyMessage(StringKeyMessage(key)) - moeumGestureProcessor.resolveMoeumList()?.let { - sendKeyMessage(StringKeyMessage(it)) + if (isLongPressed) { + isLongPressed = false + val phraseKey = quickPhraseKey + if (phraseKey != null) { + when (quickPhraseMenuPopup?.confirmAndDismiss()) { + QuickPhraseMenuPopup.MenuItem.PHRASE_PREVIEW -> { + sendKeyMessage(StringKeyMessage(QuickPhraseRepository.getPhrase(context, phraseKey))) + } + QuickPhraseMenuPopup.MenuItem.EDIT -> { + val intent = Intent(context, QuickPhraseEditActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + putExtra(QuickPhraseEditActivity.EXTRA_KEY, phraseKey.name) + } + context.startActivity(intent) + } + else -> Unit + } + } + } else if (!hasMoved && quickPhraseKey != null) { + val phrase = QuickPhraseRepository.getPhrase(context, quickPhraseKey) + sendKeyMessage(StringKeyMessage(phrase)) + } else { + sendKeyMessage(StringKeyMessage(key)) + moeumGestureProcessor.resolveMoeumList()?.let { + sendKeyMessage(StringKeyMessage(it)) + } } } MotionEvent.ACTION_CANCEL -> { + handler.removeCallbacks(longPressRunnable) previewController?.hide() + quickPhraseMenuPopup?.dismiss() + isLongPressed = false } } return super.onTouch(view, motionEvent) } - -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/pe/aioo/openmoa/view/preview/QuickPhraseMenuPopup.kt b/app/src/main/kotlin/pe/aioo/openmoa/view/preview/QuickPhraseMenuPopup.kt new file mode 100644 index 0000000..faee02a --- /dev/null +++ b/app/src/main/kotlin/pe/aioo/openmoa/view/preview/QuickPhraseMenuPopup.kt @@ -0,0 +1,74 @@ +package pe.aioo.openmoa.view.preview + +import android.content.Context +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.PopupWindow +import android.widget.TextView +import pe.aioo.openmoa.R + +class QuickPhraseMenuPopup(context: Context) { + + enum class MenuItem { PHRASE_PREVIEW, EDIT, CANCEL } + + private val popupView: View = LayoutInflater.from(context).inflate( + R.layout.quick_phrase_menu_popup, null + ) + private val phrasePreviewItem: TextView = popupView.findViewById(R.id.phrasePreviewItem) + private val editItem: TextView = popupView.findViewById(R.id.editItem) + private val cancelItem: TextView = popupView.findViewById(R.id.cancelItem) + private val popup = PopupWindow( + popupView, + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + false + ).apply { + isTouchable = false + } + private var selectedItem = MenuItem.PHRASE_PREVIEW + + val isShowing: Boolean get() = popup.isShowing + + fun show(anchor: View, phrase: String) { + if (!anchor.isAttachedToWindow) return + phrasePreviewItem.text = if (phrase.length > 5) phrase.substring(0, 5) + "…" else phrase + selectedItem = MenuItem.PHRASE_PREVIEW + updateHighlight() + popupView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED) + val xoff = anchor.width / 2 - popupView.measuredWidth / 2 + val yoff = -(anchor.height + popupView.measuredHeight) + if (popup.isShowing) { + popup.update(anchor, xoff, yoff, -1, -1) + } else { + popup.showAsDropDown(anchor, xoff, yoff, Gravity.NO_GRAVITY) + } + } + + fun updateSelectionByDelta(dx: Float, anchorWidth: Int) { + val threshold = anchorWidth / 3f + selectedItem = when { + dx < -threshold -> MenuItem.PHRASE_PREVIEW + dx > threshold -> MenuItem.CANCEL + else -> MenuItem.EDIT + } + updateHighlight() + } + + fun confirmAndDismiss(): MenuItem { + val item = selectedItem + dismiss() + return item + } + + fun dismiss() { + if (popup.isShowing) popup.dismiss() + } + + private fun updateHighlight() { + phrasePreviewItem.alpha = if (selectedItem == MenuItem.PHRASE_PREVIEW) 1.0f else 0.4f + editItem.alpha = if (selectedItem == MenuItem.EDIT) 1.0f else 0.4f + cancelItem.alpha = if (selectedItem == MenuItem.CANCEL) 1.0f else 0.4f + } +} diff --git a/app/src/main/res/layout/activity_quick_phrase_edit.xml b/app/src/main/res/layout/activity_quick_phrase_edit.xml new file mode 100644 index 0000000..293dcc8 --- /dev/null +++ b/app/src/main/res/layout/activity_quick_phrase_edit.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + +