From c9e5707803fc99afce36dbb7d7087ba4472e4f0f Mon Sep 17 00:00:00 2001 From: Dimo Karaivanov Date: Thu, 25 Apr 2024 10:40:29 +0300 Subject: [PATCH] Keypad navigation in the Settings (#499) --- .../tt9/preferences/PreferencesActivity.java | 47 ++++--- .../screens/BaseScreenFragment.java | 20 ++- .../tt9/ui/ActivityWithNavigation.java | 126 ++++++++++++++++++ 3 files changed, 176 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/io/github/sspanak/tt9/ui/ActivityWithNavigation.java diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/PreferencesActivity.java b/app/src/main/java/io/github/sspanak/tt9/preferences/PreferencesActivity.java index 31eccf4e..1355889b 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/PreferencesActivity.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/PreferencesActivity.java @@ -6,8 +6,8 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; +import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.preference.Preference; @@ -28,17 +28,14 @@ import io.github.sspanak.tt9.preferences.screens.hotkeys.HotkeysScreen; import io.github.sspanak.tt9.preferences.screens.keypad.KeyPadScreen; import io.github.sspanak.tt9.preferences.screens.languages.LanguagesScreen; import io.github.sspanak.tt9.preferences.screens.setup.SetupScreen; -import io.github.sspanak.tt9.preferences.settings.SettingsStore; +import io.github.sspanak.tt9.ui.ActivityWithNavigation; import io.github.sspanak.tt9.util.Logger; import io.github.sspanak.tt9.util.SystemSettings; -public class PreferencesActivity extends AppCompatActivity implements PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { - private SettingsStore settings; - - +public class PreferencesActivity extends ActivityWithNavigation implements PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { @Override protected void onCreate(Bundle savedInstanceState) { - settings = new SettingsStore(this); + getSettings(); applyTheme(); Logger.setLevel(settings.getLogLevel()); @@ -87,6 +84,31 @@ public class PreferencesActivity extends AppCompatActivity implements Preference } + @Override + public void onBackPressed() { + super.onBackPressed(); + Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.preferences_container); + if (fragment instanceof BaseScreenFragment) { + getOptionsCount = ((BaseScreenFragment) fragment)::getPreferenceCount; + } + } + + + @Override + protected void selectOption(int position, boolean click) { + // for convenience, scroll to the bottom on 0-key click + try { + if (position == 0) { + position = getOptionsCount.call(); + resetKeyRepeat(); // ... but do not activate the last option on double click + } + } + catch (Exception ignore) {} + + super.selectOption(position, click); + } + + /** * getScreenName * Determines the name of the screen for the given preference, as defined in the preference's "fragment" attribute. @@ -136,6 +158,8 @@ public class PreferencesActivity extends AppCompatActivity implements Preference * Replaces the currently displayed screen fragment with a new one. */ private void displayScreen(BaseScreenFragment screen, boolean addToBackStack) { + getOptionsCount = screen::getPreferenceCount; + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); transaction.replace(R.id.preferences_container, screen); @@ -168,15 +192,6 @@ public class PreferencesActivity extends AppCompatActivity implements Preference } - public SettingsStore getSettings() { - if (settings == null) { - settings = new SettingsStore(this); - } - - return settings; - } - - private void applyTheme() { AppCompatDelegate.setDefaultNightMode(settings.getTheme()); } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/BaseScreenFragment.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/BaseScreenFragment.java index ba03a747..e927094c 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/BaseScreenFragment.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/BaseScreenFragment.java @@ -4,10 +4,13 @@ import android.os.Bundle; import android.view.MenuItem; import androidx.annotation.NonNull; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; -import io.github.sspanak.tt9.util.Logger; import io.github.sspanak.tt9.preferences.PreferencesActivity; +import io.github.sspanak.tt9.util.Logger; abstract public class BaseScreenFragment extends PreferenceFragmentCompat { protected PreferencesActivity activity; @@ -69,6 +72,21 @@ abstract public class BaseScreenFragment extends PreferenceFragmentCompat { } + public int getPreferenceCount() { + PreferenceScreen screen = getPreferenceScreen(); + + int count = 0; + for (int i = screen.getPreferenceCount(); i > 0; i--) { + Preference pref = screen.getPreference(i - 1); + if (pref.isVisible()) { + count += pref instanceof PreferenceCategory ? ((PreferenceCategory) pref).getPreferenceCount() : 1; + } + } + + return count; + } + + abstract public String getName(); abstract protected int getTitle(); abstract protected int getXml(); diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/ActivityWithNavigation.java b/app/src/main/java/io/github/sspanak/tt9/ui/ActivityWithNavigation.java new file mode 100644 index 00000000..84fdb6a7 --- /dev/null +++ b/app/src/main/java/io/github/sspanak/tt9/ui/ActivityWithNavigation.java @@ -0,0 +1,126 @@ +package io.github.sspanak.tt9.ui; + +import android.os.Bundle; +import android.os.PersistableBundle; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.inputmethod.BaseInputConnection; +import android.view.inputmethod.InputConnection; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import java.util.concurrent.Callable; + +import io.github.sspanak.tt9.ime.helpers.Key; +import io.github.sspanak.tt9.preferences.settings.SettingsStore; +import io.github.sspanak.tt9.util.Logger; + +abstract public class ActivityWithNavigation extends AppCompatActivity { + public static final String LOG_TAG = ActivityWithNavigation.class.getSimpleName(); + + protected SettingsStore settings; + protected Callable getOptionsCount; + private int lastKey = KeyEvent.KEYCODE_UNKNOWN; + + + @Override + public void onCreate(@Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) { + super.onCreate(savedInstanceState, persistentState); + getSettings(); + } + + + @Override + final public boolean onKeyDown(int keyCode, KeyEvent event) { + // ignore our own key events + if (event.getSource() == InputDevice.SOURCE_UNKNOWN) { + return super.onKeyDown(keyCode, event); + } + + // Reset the last key even if we are not going to process it. This is to avoid + // detecting a double click, when the user has pressed a different key in between. + boolean click = (keyCode == lastKey); + lastKey = keyCode; + + if (!Key.isNumber(keyCode)) { + return super.onKeyDown(keyCode, event); + } + + selectOption(Key.codeToNumber(settings, keyCode), click); + return true; + } + + + @Override + final public boolean onKeyUp(int keyCode, KeyEvent event) { + return Key.isNumber(keyCode) || super.onKeyUp(keyCode, event); + } + + + final public SettingsStore getSettings() { + if (settings == null) { + settings = new SettingsStore(this); + } + + return settings; + } + + + protected void resetKeyRepeat() { + lastKey = KeyEvent.KEYCODE_UNKNOWN; + } + + + /** + * Simulates a click on the option at the given position. Positions are 1-based. + */ + protected void selectOption(int position, boolean click) { + int optionsCount; + + try { + optionsCount = getOptionsCount.call(); + } catch (Exception e) { + Logger.e(LOG_TAG, "Keypad navigation not possible. Failed to get options count. " + e); + return; + } + + if (position <= 0 || position > optionsCount) { + return; + } + + BaseInputConnection inputConnection = new BaseInputConnection(getWindow().getDecorView(), true); + + // Scroll to the bottom to make sure we have a correct base for counting to the desired position + // Scrolling to top, then down to the position is not possible, because some phones allow + // selecting the Back button, but others don't. + scroll(inputConnection, optionsCount + 1, false); + scroll(inputConnection, optionsCount - position, true); + + if (click) { + clickSelected(inputConnection); + } + } + + + private void scroll(@NonNull InputConnection connection, int positions, boolean up) { + KeyEvent press = new KeyEvent(KeyEvent.ACTION_DOWN, up ? KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN); + KeyEvent release = new KeyEvent(KeyEvent.ACTION_UP, up ? KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN); + press.setSource(InputDevice.SOURCE_UNKNOWN); + release.setSource(InputDevice.SOURCE_UNKNOWN); + + for (int i = 0; i < positions; i++) { + connection.sendKeyEvent(press); + connection.sendKeyEvent(release); + } + } + + + private void clickSelected(@NonNull InputConnection connection) { + KeyEvent enterPress = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER); + KeyEvent enterRelease = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER); + connection.sendKeyEvent(enterPress); + connection.sendKeyEvent(enterRelease); + } +}