diff --git a/HISTORY.md b/HISTORY.md index e348de89f3..386f77f017 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,30 @@ # Keyman Version History +## 19.0.266 alpha 2026-08-13 + +* chore(deps-dev): bump esbuild from 0.25.0 to 0.28.1 (#16095) +* feat(web): add `keyman.versionInfo` API (#16351) +* fix(web): cleanup `loaduserinterface` and `unloaduserinterface` events (#16353) +* docs(web): deprecate unused and obsolete API endpoints and docs cleanup (#16354) +* chore(common): cleanup and merge Preprocessor constants (#14360) + +## 19.0.265 alpha 2026-08-12 + +* fix(core): make coherence check a debug assert (#16343) +* feat(windows): honour Windows "metered connection" flag for downloading updates (#16099) + +## 19.0.264 alpha 2026-08-11 + +* docs(android): update keyboardharness readme (#16220) +* feat(android): design improvement for settings, add dark mode and tool menu (#15827) + +## 19.0.263 alpha 2026-08-10 + +* chore(deps): bump tar from 7.5.11 to 7.5.22 in /developer/src/server/src/win32/trayicon/addon-src (#16282) +* chore(deps): bump brace-expansion from 5.0.6 to 5.0.9 in /developer/src/server/src/win32/trayicon/addon-src (#16295) +* refactor(web): improve naming of `_SetTargetDir` function (#16311) +* docs(web): add language identifier to the code blocks of event docs (#16333) + ## 19.0.262 alpha 2026-08-04 * chore(web): fire `keyboardloaded` event only once per keyboard (#16297) diff --git a/VERSION.md b/VERSION.md index f00ee4d41a..3a603912f4 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.263 \ No newline at end of file +19.0.267 \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/AndroidManifest.xml b/android/KMAPro/kMAPro/src/main/AndroidManifest.xml index bdb1d18a43..563690c031 100644 --- a/android/KMAPro/kMAPro/src/main/AndroidManifest.xml +++ b/android/KMAPro/kMAPro/src/main/AndroidManifest.xml @@ -17,21 +17,22 @@ https://support.google.com/googleplay/android-developer/answer/9214102# --> + android:supportsRtl="true" + > - @@ -65,7 +66,7 @@ android:exported="true" android:label="@string/app_name" android:resizeableActivity="false" - android:theme="@style/AppTheme.BrandedLaunch"> + android:theme="@style/Theme.App.Starting"> @@ -241,23 +242,17 @@ android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize" android:label="@string/app_name" android:launchMode="singleTask" - android:theme="@style/AppTheme.Light.Dialog"/> - + android:theme="@style/AppTheme.Dialog"/> + android:theme="@style/AppTheme.Dialog" /> -
+ + +
- + +
diff --git a/android/KMAPro/kMAPro/src/main/assets/banner/keyman_banner_dark.png b/android/KMAPro/kMAPro/src/main/assets/banner/keyman_banner_dark.png new file mode 100644 index 0000000000..644af21a2d Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/assets/banner/keyman_banner_dark.png differ diff --git a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/BannerController.java b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/BannerController.java index f716faa9aa..5c5a16e597 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/BannerController.java +++ b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/BannerController.java @@ -13,7 +13,7 @@ public class BannerController { public static final String KM_BANNER_DIR = "banner"; public static final String KM_BANNER_THEME_KEYMAN = "banner.html"; - public static void setHTMLBanner(Context context, KMManager.KeyboardType keyboardType) { + public static void setHTMLBanner(Context context, KMManager.KeyboardType keyboardType, boolean isDarkMode) { if (keyboardType == KMManager.KeyboardType.KEYBOARD_TYPE_UNDEFINED) { return; } @@ -22,6 +22,9 @@ public class BannerController { // Always use Keyman banner theme String contents = FileUtils.readContents(context, KM_BANNER_THEME_KEYMAN); + if (isDarkMode) { + contents = contents.replace("class=\"keyman-banner\"", "class=\"keyman-banner dark\""); + } KMManager.setHTMLBanner(keyboardType, contents); KMManager.setBanner(keyboardType, KMManager.BannerType.HTML); KMManager.showBanner(true); diff --git a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java index 16330bfcda..c3939c03f8 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java +++ b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java @@ -7,7 +7,7 @@ package com.keyman.android; import com.keyman.engine.util.DownloadFileUtils; import com.tavultesoft.kmapro.BuildConfig; import com.tavultesoft.kmapro.DefaultLanguageResource; -import com.tavultesoft.kmapro.KeymanSettingsActivity; +import com.tavultesoft.kmapro.KeymanSettingsKeys; import com.tavultesoft.kmapro.PreferencesManager; import com.keyman.engine.KMManager; import com.keyman.engine.KMManager.KeyboardType; @@ -76,13 +76,13 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven KMManager.setInputMethodService(this); // for HW interface SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); - KMManager.SpacebarText spacebarText = KMManager.SpacebarText.fromString(prefs.getString(KeymanSettingsActivity.spacebarTextKey, KMManager.SpacebarText.LANGUAGE_KEYBOARD.toString())); + KMManager.SpacebarText spacebarText = KMManager.SpacebarText.fromString(prefs.getString(KeymanSettingsKeys.SPACEBAR_TEXT, KMManager.SpacebarText.LANGUAGE_KEYBOARD.toString())); KMManager.setSpacebarText(spacebarText); // Set the system keyboard HTML banner - BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM); + BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM, isDarkMode()); - boolean mayHaveHapticFeedback = prefs.getBoolean(KeymanSettingsActivity.hapticFeedbackKey, false); + boolean mayHaveHapticFeedback = prefs.getBoolean(KeymanSettingsKeys.HAPTIC_FEEDBACK, false); KMManager.setHapticFeedback(mayHaveHapticFeedback); // Checking for updates should never be allowed to crash the keyboard. @@ -176,7 +176,7 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven if (KMManager.getPredictionsSuspended(KeyboardType.KEYBOARD_TYPE_SYSTEM)) { KMManager.setBannerOptions(false); // Set the system keyboard HTML banner - BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM); + BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM, isDarkMode()); } else if (KMManager.isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM)){ // Check if predictions needs to be re-enabled per Settings preference Keyboard kbInfo = KMManager.getCurrentKeyboardInfo(appContext); @@ -282,7 +282,7 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven @Override public void onKeyboardChanged(String newKeyboard) { // Refresh banner theme - BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM); + BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM, isDarkMode()); KMManager.showSystemKeyboard(); sendCurrentFontName(); } @@ -290,7 +290,7 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven @Override public void onKeyboardShown() { // Refresh banner theme - BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM); + BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM, isDarkMode()); } @Override @@ -309,7 +309,7 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven Context context = getApplicationContext(); SharedPreferences prefs = context.getSharedPreferences(PreferencesManager.kma_prefs_name, Context.MODE_PRIVATE); - boolean showOSK = prefs.getBoolean(KeymanSettingsActivity.oskWithPhysicalKeyboardKey, false); + boolean showOSK = prefs.getBoolean(KeymanSettingsKeys.OSK_WITH_PHYSICAL_KEYBOARD, false); return showOSK; } @@ -349,4 +349,8 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven public boolean onKeyLongPress(int keyCode, KeyEvent event) { return interpreter.onKeyLongPress(keyCode, event); } + private boolean isDarkMode() { + return (getResources().getConfiguration().uiMode & + Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + } } \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/GetStartedActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/GetStartedActivity.java index 59558fdb1f..2fc9e90d41 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/GetStartedActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/GetStartedActivity.java @@ -16,7 +16,7 @@ import android.os.Bundle; import android.provider.Settings; import android.view.View; import android.view.Window; -import android.view.inputmethod.InputMethodInfo; + import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.CheckBox; @@ -24,7 +24,7 @@ import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.ImageButton; import android.widget.ListView; -import androidx.appcompat.app.AppCompatActivity; + import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -36,14 +36,25 @@ public class GetStartedActivity extends BaseActivity { private static ArrayList> list = null; private static KMListAdapter listAdapter = null; protected static final String showGetStartedKey = "ShowGetStarted"; + public static final String openDefaultKeyboardPickerOnlyKey = "OpenDefaultKeyboardPickerOnly"; private final String iconKey = "icon"; private final String textKey = "text"; private final String isEnabledKey = "isEnabled"; + private boolean openDefaultKeyboardPickerOnly = false; + private boolean didAutoOpenDefaultKeyboardPicker = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); supportRequestWindowFeature(Window.FEATURE_NO_TITLE); + openDefaultKeyboardPickerOnly = getIntent().getBooleanExtra(openDefaultKeyboardPickerOnlyKey, false); + + if (openDefaultKeyboardPickerOnly) { + // Picker-only mode: intentionally skip rendering Get Started UI. + KMManager.dontCloseParentAppOnShowKeyboardPicker(); + return; + } + final Context context = this; setContentView(R.layout.get_started_list_layout); @@ -161,20 +172,40 @@ public class GetStartedActivity extends BaseActivity { super.onResume(); } - @Override - protected void onPause() { - super.onPause(); - } - @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { + if (openDefaultKeyboardPickerOnly) { + + // When the user taps 'Set Default Keyboard' from the drawer, + // GetStartedActivity is launched in picker-only mode. But you + // can't open the system keyboard picker directly from a menu + // tap — it requires a window with focus. So we use + // onWindowFocusChanged as the trigger point: first focus opens + // the picker, and when the user dismisses it and focus returns + // to this activity, we call finish() to close it. The activity + // is essentially just a invisible bridge to show the system + // picker. + + if (!didAutoOpenDefaultKeyboardPicker) { + didAutoOpenDefaultKeyboardPicker = true; + InputMethodManager imManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + if (imManager != null) { + imManager.showInputMethodPicker(); + } + } else { + // Focus returned after the picker was dismissed, so close the host activity. + finish(); + } + return; + } + // Enumerated steps are replaced with checkboxes when the user completes a step String one = String.valueOf(R.drawable.ic_looks_one); String two = String.valueOf(R.drawable.ic_looks_two); String three = String.valueOf(R.drawable.ic_looks_three); - String checkbox_on = String.valueOf(android.R.drawable.checkbox_on_background); + String checkbox_on = String.valueOf(R.drawable.ic_checkbox); String info = String.valueOf(R.drawable.ic_info_outline); List kbList = KMManager.getKeyboardsList(this); @@ -208,4 +239,12 @@ public class GetStartedActivity extends BaseActivity { uncheckGetStartedIfComplete(); } } + + @Override + protected void onPause() { + super.onPause(); + if (openDefaultKeyboardPickerOnly && didAutoOpenDefaultKeyboardPicker) { + finish(); + } + } } \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java index 2a2b85a32d..e6a2a597cf 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java @@ -81,7 +81,7 @@ public class KMPBrowserActivity extends BaseActivity { } setupEdgeToEdge(R.id.kmp_browser_layout); - setupStatusBarColors(R.color.complementary_5, android.R.color.white); + setupStatusBarColors(R.color.keyman_blue, android.R.color.white); webView = (WebView) findViewById(R.id.kmpBrowserWebView); webView.getSettings().setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeyboardSettingsActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeyboardSettingsActivity.java index 689548a56e..a41690b254 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeyboardSettingsActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeyboardSettingsActivity.java @@ -96,7 +96,7 @@ public final class KeyboardSettingsActivity extends AppCompatActivity { // Display notification to download update if available if (kbd.hasUpdateAvailable()) { hashMap.put(subtitleKey, context.getString(R.string.update_available, kbVersion)); - icon = String.valueOf(R.drawable.ic_cloud_download); + icon = String.valueOf(R.drawable.ic_cloud_download_mode); } hashMap.put(iconKey, icon); infoList.add(hashMap); @@ -105,7 +105,7 @@ public final class KeyboardSettingsActivity extends AppCompatActivity { hashMap = new HashMap<>(); final String customHelpLink = kbd.getHelpLink(); // Check if app declared FileProvider - icon = String.valueOf(R.drawable.ic_action_forward); + icon = String.valueOf(R.drawable.ic_action_forward_mode); // Don't show help link arrow if File Provider unavailable, or custom help doesn't exist if ( (customHelpLink != null && !FileProviderUtils.exists(context)) || (customHelpLink == null && !packageID.equals(KMManager.KMDefault_UndefinedPackageID)) ) { diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanApplication.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanApplication.java new file mode 100644 index 0000000000..3fb3588f5a --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanApplication.java @@ -0,0 +1,25 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by MengchouChey on 2026-06-10 + * Internship Program, 9th Feb - 27th Apr 2026 + * + * This file is the Application class for Keyman, which is used to set the default night mode based on user preferences when the application starts. + * + */ +package com.tavultesoft.kmapro; + +import android.app.Application; +import android.content.SharedPreferences; + +import androidx.appcompat.app.AppCompatDelegate; + +public class KeymanApplication extends Application { + @Override + public void onCreate() { + SharedPreferences prefs = getSharedPreferences("settings", MODE_PRIVATE); + int mode = prefs.getInt("theme_mode", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + AppCompatDelegate.setDefaultNightMode(mode); + super.onCreate(); + } +} diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsActivity.java deleted file mode 100644 index bda56856b2..0000000000 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsActivity.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.tavultesoft.kmapro; - -import android.os.Bundle; - -import com.keyman.engine.BaseActivity; -import com.keyman.engine.KMManager; - -import androidx.appcompat.widget.Toolbar; - -public class KeymanSettingsActivity extends BaseActivity { - protected static final String installedLanguagesKey = "InstalledLanguages"; - protected static final String installKeyboardOrDictionaryKey = "InstallKeyboardOrDictionary"; - protected static final String sendCrashReport = "SendCrashReport"; - public static final String spacebarTextKey = "SpacebarText"; - public static final String hapticFeedbackKey = "HapticFeedback"; - public static final String oskWithPhysicalKeyboardKey = "ShowOSK"; - - protected KeymanSettingsFragment innerFragment; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.keyman_settings_layout); - - setupEdgeToEdge(R.id.keyman_settings_layout); - setupStatusBarColors(R.color.keyman_blue, android.R.color.white); - - Toolbar toolbar = (Toolbar) findViewById(R.id.keyman_settings_toolbar); - toolbar.setTitleTextColor(getResources().getColor(android.R.color.white)); - toolbar.setTitle(R.string.keyman_settings); - setSupportActionBar(toolbar); - if (getSupportActionBar() != null) { - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setDisplayShowHomeEnabled(true); - getSupportActionBar().setDisplayShowTitleEnabled(true); - } - - innerFragment = (KeymanSettingsFragment) getSupportFragmentManager().findFragmentById(R.id.keyman_settings_fragment); - - // For Keyman sites, disable keyboard picker task flag so keyboard picker doesn't dismiss Keyman app - KMManager.dontCloseParentAppOnShowKeyboardPicker(); - } - - @Override - public void onWindowFocusChanged(boolean hasFocus) { - super.onWindowFocusChanged(hasFocus); - - if(hasFocus) { - innerFragment.update(); - } - } -} diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java index 3aa149f477..e1819dd97f 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java @@ -44,7 +44,7 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat { getPreferenceManager().setSharedPreferencesName(getString(R.string.kma_prefs_name)); languagesPreference = new Preference(context); - languagesPreference.setKey(KeymanSettingsActivity.installedLanguagesKey); + languagesPreference.setKey(KeymanSettingsKeys.INSTALLED_LANGUAGES); languagesPreference.setTitle(getInstalledLanguagesText()); languagesPreference.setWidgetLayoutResource(R.layout.preference_icon_layout); Intent languagesIntent = new Intent(context, LanguagesSettingsActivity.class); @@ -52,7 +52,7 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat { languagesPreference.setIntent(languagesIntent); installKeyboardOrDictionary = new Preference(context); - installKeyboardOrDictionary.setKey(KeymanSettingsActivity.installKeyboardOrDictionaryKey); + installKeyboardOrDictionary.setKey(KeymanSettingsKeys.INSTALL_KEYBOARD_OR_DICTIONARY); installKeyboardOrDictionary.setTitle(getString(R.string.install_keyboard_or_dictionary)); installKeyboardOrDictionary.setWidgetLayoutResource(R.layout.preference_add_icon_layout); Intent installIntent = new Intent(context, KeymanSettingsInstallActivity.class); @@ -75,7 +75,7 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat { }; setSystemKeyboardPreference = new CheckBoxPreference(context); - setSystemKeyboardPreference.setTitle(R.string.enable_system_keyboard); + setSystemKeyboardPreference.setTitle(R.string.system_keyboards); setSystemKeyboardPreference.setSingleLineTitle(false); setSystemKeyboardPreference.setDefaultValue(SystemIMESettings.isEnabledAsSystemKB(context)); setSystemKeyboardPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { @@ -120,7 +120,7 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat { /* Spacebar Caption Preference */ spacebarTextPreference = new ListPreference(context); - spacebarTextPreference.setKey(KeymanSettingsActivity.spacebarTextKey); + spacebarTextPreference.setKey(KeymanSettingsKeys.SPACEBAR_TEXT); spacebarTextPreference.setTitle(getString(R.string.spacebar_caption)); // getString(R.string.change_display_language)); @@ -161,7 +161,7 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat { }); SwitchPreference oskWithPhysicalKeyboardPreference = new SwitchPreference(context); - oskWithPhysicalKeyboardPreference.setKey(KeymanSettingsActivity.oskWithPhysicalKeyboardKey); + oskWithPhysicalKeyboardPreference.setKey(KeymanSettingsKeys.OSK_WITH_PHYSICAL_KEYBOARD); oskWithPhysicalKeyboardPreference.setTitle(getString(R.string.show_osk)); oskWithPhysicalKeyboardPreference.setSummary(getString(R.string.show_osk_hint)); @@ -169,7 +169,7 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat { PreferencesManager.kma_prefs_name, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); boolean showOSK = prefs.getBoolean( - KeymanSettingsActivity.oskWithPhysicalKeyboardKey, false); + KeymanSettingsKeys.OSK_WITH_PHYSICAL_KEYBOARD, false); oskWithPhysicalKeyboardPreference.setDefaultValue(showOSK); oskWithPhysicalKeyboardPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @@ -179,14 +179,14 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat { return false; } - editor.putBoolean(KeymanSettingsActivity.oskWithPhysicalKeyboardKey, (boolean)newValue); + editor.putBoolean(KeymanSettingsKeys.OSK_WITH_PHYSICAL_KEYBOARD, (boolean)newValue); editor.commit(); return true; } }); SwitchPreference hapticFeedbackPreference = new SwitchPreference(context); - hapticFeedbackPreference.setKey(KeymanSettingsActivity.hapticFeedbackKey); + hapticFeedbackPreference.setKey(KeymanSettingsKeys.HAPTIC_FEEDBACK); hapticFeedbackPreference.setTitle(getString(R.string.haptic_feedback)); hapticFeedbackPreference.setDefaultValue(false); hapticFeedbackPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { @@ -212,7 +212,7 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat { getStartedPreference.setDefaultValue(true); SwitchPreference sendCrashReportPreference = new SwitchPreference(context); - sendCrashReportPreference.setKey(KeymanSettingsActivity.sendCrashReport); + sendCrashReportPreference.setKey(KeymanSettingsKeys.SEND_CRASH_REPORT); sendCrashReportPreference.setTitle(getString(R.string.show_send_crash_report)); sendCrashReportPreference.setSummaryOn(getString(R.string.show_send_crash_report_on)); sendCrashReportPreference.setSummaryOff(getString(R.string.show_send_crash_report_off)); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsInstallActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsInstallActivity.java index 3a7a9ce8a7..1775dd0267 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsInstallActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsInstallActivity.java @@ -21,7 +21,6 @@ import android.view.Window; import android.widget.AdapterView; import android.widget.ListAdapter; import android.widget.ListView; -import android.os.Bundle; import android.widget.SimpleAdapter; import android.widget.TextView; @@ -71,13 +70,13 @@ public class KeymanSettingsInstallActivity extends BaseActivity { final String noIcon = "0"; HashMap hashMap = new HashMap<>(); hashMap.put(titleKey, getString(R.string.install_from_keyman_dot_com)); - String icon = String.valueOf(R.drawable.ic_content_add); + String icon = String.valueOf(R.drawable.ic_content_add_mode); hashMap.put(iconKey, icon); installOptionList.add(hashMap); hashMap = new HashMap<>(); hashMap.put(titleKey, getString(R.string.install_from_local_file)); - icon = String.valueOf(R.drawable.ic_folder_open); + icon = String.valueOf(R.drawable.ic_folder_open_mode); hashMap.put(iconKey, icon); hashMap.put(isEnabledKey, "true"); installOptionList.add(hashMap); @@ -95,7 +94,7 @@ public class KeymanSettingsInstallActivity extends BaseActivity { hashMap = new HashMap<>(); hashMap.put(titleKey, getString(R.string.add_languages_to_installed_keyboard)); hashMap.put(subtitleKey, getString(R.string.add_language_subtext)); - icon = String.valueOf(R.drawable.ic_content_add); + icon = String.valueOf(R.drawable.ic_content_add_mode); hashMap.put(iconKey, icon); hashMap.put(isEnabledKey, "true"); installOptionList.add(hashMap); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsKeys.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsKeys.java new file mode 100644 index 0000000000..9055307b6d --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsKeys.java @@ -0,0 +1,22 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by MengchouChey on 2026-06-10 + * Internship Program, 9th Feb - 27th Apr 2026 + * + * These are the keys used to replace KeymanSettingsActivity and KeymanSettingsFragment + * + */ +package com.tavultesoft.kmapro; + +public final class KeymanSettingsKeys { + private KeymanSettingsKeys() { + } + + public static final String INSTALLED_LANGUAGES = "InstalledLanguages"; + public static final String INSTALL_KEYBOARD_OR_DICTIONARY = "InstallKeyboardOrDictionary"; + public static final String SEND_CRASH_REPORT = "SendCrashReport"; + public static final String SPACEBAR_TEXT = "SpacebarText"; + public static final String HAPTIC_FEEDBACK = "HapticFeedback"; + public static final String OSK_WITH_PHYSICAL_KEYBOARD = "ShowOSK"; +} diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java index 9342a0b528..887cfb7033 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java @@ -153,7 +153,7 @@ public final class LanguageSettingsActivity extends BaseActivity { updateActiveLexicalModel(); ImageView imageView = (ImageView) layout.findViewById(R.id.image1); - imageView.setImageResource(R.drawable.ic_action_forward); + imageView.setImageResource(R.drawable.ic_action_forward_mode); layout.setEnabled(true); layout.setOnClickListener(new View.OnClickListener() { @Override @@ -333,7 +333,7 @@ public final class LanguageSettingsActivity extends BaseActivity { } holder.text.setText(kbd.getResourceName()); - holder.img.setImageResource(R.drawable.ic_action_forward); + holder.img.setImageResource(R.drawable.ic_action_forward_mode); return convertView; } diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguagesSettingsActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguagesSettingsActivity.java index 79c845fa6f..3e3c6c785f 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguagesSettingsActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguagesSettingsActivity.java @@ -205,7 +205,7 @@ public final class LanguagesSettingsActivity extends BaseActivity { } holder.textLang.setText(data.name); - holder.img.setImageResource(R.drawable.ic_action_forward); + holder.img.setImageResource(R.drawable.ic_action_forward_mode); holder.textCount.setText(getContext().getResources().getQuantityString(R.plurals.keyboard_count, data.keyboards.size(), data.keyboards.size())); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java index 01f38a4a66..044f21abba 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java @@ -6,6 +6,8 @@ package com.tavultesoft.kmapro; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import com.google.android.material.navigation.NavigationView; +import android.util.DisplayMetrics; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; @@ -26,6 +28,7 @@ import com.keyman.engine.KMHelpFileActivity; import com.keyman.engine.KMKeyboardDownloaderActivity; import com.keyman.engine.KMManager; import com.keyman.engine.KMManager.KeyboardType; +import com.keyman.engine.KeyboardEventHandler; import com.keyman.engine.KmpInstallMode; import com.keyman.engine.KMTextView; import com.keyman.engine.KeyboardEventHandler.OnKeyboardDownloadEventListener; @@ -36,7 +39,6 @@ import com.keyman.engine.cloud.impl.CloudLexicalModelMetaDataDownloadCallback; import com.keyman.engine.data.CloudRepository; import com.keyman.engine.data.Dataset; import com.keyman.engine.data.Keyboard; -import com.keyman.engine.data.LexicalModel; import com.keyman.engine.util.FileUtils; import com.keyman.engine.util.DownloadFileUtils; import com.keyman.android.DownloadIntentService; @@ -47,17 +49,15 @@ import com.keyman.engine.util.WebViewUtils; import com.keyman.engine.util.WebViewUtils.EngineWebViewVersionStatus; import com.keyman.engine.util.WebViewUtils.SystemWebViewStatus; -import android.Manifest; import android.app.ProgressDialog; import android.content.pm.PackageManager; +import android.graphics.Color; import android.net.Uri; import android.net.Uri.Builder; import android.os.Build; import android.os.Bundle; -import android.os.Environment; import android.os.Handler; import android.os.ParcelFileDescriptor; -import android.os.Parcelable; import android.annotation.SuppressLint; import android.annotation.TargetApi; @@ -65,7 +65,10 @@ import androidx.activity.result.ActivityResult; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.widget.Toolbar; +import androidx.appcompat.widget.SwitchCompat; import androidx.appcompat.app.AlertDialog; import androidx.constraintlayout.widget.ConstraintLayout; import android.content.ClipData; @@ -73,11 +76,10 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.ResolveInfo; + import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.Paint; import android.graphics.Point; import android.graphics.Rect; @@ -86,30 +88,61 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.core.app.ActivityCompat; +import androidx.core.view.MenuCompat; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsControllerCompat; +import androidx.drawerlayout.widget.DrawerLayout; + +import androidx.recyclerview.widget.RecyclerView; + +import androidx.core.content.ContextCompat; import android.provider.Settings; import android.text.Html; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; +import android.text.style.RelativeSizeSpan; import android.util.Log; import android.util.TypedValue; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; +import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; import android.widget.Button; +import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.SeekBar; import android.widget.TextView; import android.widget.Toast; +import android.view.Gravity; +import android.view.inputmethod.InputMethodManager; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.appcompat.app.ActionBarDrawerToggle; +import com.google.android.material.navigation.NavigationView; +import androidx.core.content.ContextCompat; +import androidx.core.view.GravityCompat; +import android.view.MenuItem; +import androidx.appcompat.widget.AppCompatCheckBox; + import io.sentry.android.core.SentryAndroid; +import io.sentry.Sentry; public class MainActivity extends BaseActivity implements OnKeyboardEventListener, OnKeyboardDownloadEventListener, ActivityCompat.OnRequestPermissionsResultCallback { public static Context context; + private DrawerLayout drawerLayout; + private ActionBarDrawerToggle drawerToggle; + private NavigationView navigationView; + // Fields used for installing kmp packages public static final int PERMISSION_REQUEST_STORAGE = 0; public static final int READ_REQUEST_CODE = 42; @@ -118,6 +151,10 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene private ConstraintLayout constraintLayout; private KMTextView textView; + private View textPopupToggleButton; + private View keyboardToolbarContainer; + private boolean isKeyboardToolbarExpanded = false; + private boolean suppressOutsideCloseForCurrentTouch = false; private final int minTextSize = 16; private final int maxTextSize = 72; private int textSize = minTextSize; @@ -126,17 +163,31 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene private static final String userTextSizeKey = "UserTextSize"; private Toolbar toolbar; private Menu menu; + private String lastCurrentKeyboardDrawerTitle = null; + private String lastKnownKeyboardId = null; + private static final boolean DEBUG_CURRENT_KEYBOARD_LOGS = false; + private static final boolean SHOW_CURRENT_KEYBOARD_ERROR_TOAST = false; private static Dataset repo; private boolean didExecuteParser = false; DownloadResultReceiver resultReceiver; private static ProgressDialog progressDialog; + private static final String PREFS_NAME = "settings"; + private static final String KEY_THEME_MODE = "theme_mode"; @Override protected void onCreate(Bundle savedInstanceState) { - setTheme(R.style.AppTheme); + SharedPreferences theme_prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); + int savedMode = theme_prefs.getInt(KEY_THEME_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + AppCompatDelegate.setDefaultNightMode(savedMode); + getDelegate().applyDayNight(); super.onCreate(savedInstanceState); + KMManager.initialize(getApplicationContext(), KeyboardType.KEYBOARD_TYPE_INAPP); + setContentView(R.layout.activity_main); + constraintLayout = (ConstraintLayout) findViewById(R.id.constraintLayout); + + setupEdgeToEdge(R.id.constraintLayout); context = this; checkSendCrashReport(); @@ -154,24 +205,43 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene KMManager.setDebugMode(true); } - // Verify WebView installed and enabled before attempting to initialize KMManager - KMManager.initialize(getApplicationContext(), KeyboardType.KEYBOARD_TYPE_INAPP); KMManager.executeResourceUpdate(this); DefaultLanguageResource.install(context); SharedPreferences prefs = getSharedPreferences(getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); - KMManager.SpacebarText spacebarText = KMManager.SpacebarText.fromString(prefs.getString(KeymanSettingsActivity.spacebarTextKey, KMManager.SpacebarText.LANGUAGE_KEYBOARD.toString())); + KMManager.SpacebarText spacebarText = KMManager.SpacebarText.fromString(prefs.getString(KeymanSettingsKeys.SPACEBAR_TEXT, KMManager.SpacebarText.LANGUAGE_KEYBOARD.toString())); KMManager.setSpacebarText(spacebarText); checkHapticFeedback(); - setContentView(R.layout.activity_main); + NavigationView navView = findViewById(R.id.nav_view); + DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); + int width = (int) (displayMetrics.widthPixels * 0.80); + navView.getLayoutParams().width = width; + navView.requestLayout(); - constraintLayout = (ConstraintLayout)findViewById(R.id.constraintLayout); - setupEdgeToEdge(R.id.constraintLayout); - setupStatusBarColors(android.R.color.white, R.color.neutral_2); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Window window = getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + + window.setStatusBarColor(ContextCompat.getColor(this, R.color.toolbarColor)); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + int flags = window.getDecorView().getSystemUiVisibility(); + + boolean isDarkMode = isDarkMode(); + + if (!isDarkMode) { + flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; + } else { + flags &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; + } + window.getDecorView().setSystemUiVisibility(flags); + } + } toolbar = (Toolbar) findViewById(R.id.titlebar); setSupportActionBar(toolbar); @@ -180,14 +250,83 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene getSupportActionBar().setDisplayShowHomeEnabled(true); getSupportActionBar().setLogo(R.drawable.keyman_logo); getSupportActionBar().setDisplayShowTitleEnabled(false); + // For Darkmode - once switched to dark mode, the logo becomes white + getSupportActionBar().setLogo(R.drawable.keyman_logo_mode); getSupportActionBar().setBackgroundDrawable(getActionBarDrawable(this)); + drawerLayout = findViewById(R.id.drawer_layout); + drawerLayout.setScrimColor(Color.parseColor("#80000000")); + drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.drawer_open, R.string.drawer_close); + drawerLayout.addDrawerListener(drawerToggle); + drawerToggle.syncState(); + + navigationView = findViewById(R.id.nav_view); + navigationView.setItemMaxLines(4); + MenuCompat.setGroupDividerEnabled(navigationView.getMenu(), true); + navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() { + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem item) { + int id = item.getItemId(); + if (id == R.id.nav_toggle_show_osk || id == R.id.nav_toggle_send_crash_report) { + toggleDrawerSwitch(navigationView, id); + return true; + } + + drawerLayout.closeDrawers(); + return handleDrawerNavigationItemSelected(id); + } + }); + updateCurrentKeyboardDrawerItemTitle(navigationView); + updateInstalledLanguagesDrawerTitle(navigationView); + initializeDrawerItemSubtitles(navigationView); + initializeDrawerToggleOptions(navigationView); + + drawerLayout.addDrawerListener(new DrawerLayout.SimpleDrawerListener() { + + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + KMManager.hideSystemKeyboard(); + textView.dismissKeyboard(); + } + + @Override + public void onDrawerOpened(View drawerView) { + if (navigationView != null) { + updateCurrentKeyboardDrawerItemTitle(navigationView); + updateInstalledLanguagesDrawerTitle(navigationView); + initializeDrawerItemSubtitles(navigationView); + } + } + + @Override + public void onDrawerClosed(View drawerView) { + textView.callOnClick(); + if (navigationView != null) { + View menuView = navigationView.getChildAt(0); + if (menuView instanceof RecyclerView) { + ((RecyclerView) menuView).scrollToPosition(0); + } + } + } + }); + + drawerToggle.getDrawerArrowDrawable().setColor(Color.BLACK); + drawerToggle.getDrawerArrowDrawable().setBarThickness(10f); + drawerToggle.getDrawerArrowDrawable().setGapSize(10f); + textView = (KMTextView) findViewById(R.id.kmTextView); textView.setText(prefs.getString(userTextKey, "")); textSize = prefs.getInt(userTextSizeKey, minTextSize); textView.setTextSize((float) textSize); textView.setSelection(textView.getText().length()); + initializeKeyboardToolbar(); + + textView.post(() -> { + KMManager.onConfigurationChanged(this.getResources().getConfiguration()); + KMManager.getKeyboardHeight(this); + }); + // Check WebView enabled and Chrome version SystemWebViewStatus webViewStatus = WebViewUtils.getSystemWebViewStatus(context); EngineWebViewVersionStatus chromeStatus = KMManager.getEngineWebViewVersionStatus(); @@ -197,6 +336,56 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene checkGetStarted(); } + private void openThemeDialog() { + String[] options = { + getString(R.string.theme_dark_mode), + getString(R.string.theme_light_mode), + getString(R.string.theme_system_default) + }; + + SharedPreferences themePrefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); + int currentMode = themePrefs.getInt(KEY_THEME_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + + int checkedItem; + if (currentMode == AppCompatDelegate.MODE_NIGHT_YES) + checkedItem = 0; + else if (currentMode == AppCompatDelegate.MODE_NIGHT_NO) + checkedItem = 1; + else + checkedItem = 2; + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(R.string.theme_choose)); + + builder.setSingleChoiceItems(options, checkedItem, (dialog, which) -> { + int modeToApply; + switch (which) { + case 0: + modeToApply = AppCompatDelegate.MODE_NIGHT_YES; + break; + case 1: + modeToApply = AppCompatDelegate.MODE_NIGHT_NO; + break; + default: + modeToApply = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; + break; + } + // Save the choice + themePrefs.edit().putInt(KEY_THEME_MODE, modeToApply).apply(); + + // Apply immediately + AppCompatDelegate.setDefaultNightMode(modeToApply); + getDelegate().applyDayNight(); // ensures current activity updates + if (navigationView != null) { + updateThemeDrawerSubtitle(navigationView); + } + + dialog.dismiss(); + }); + builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss()); + builder.show(); + } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override public void onActivityResult(int requestCode, int resultCode, Intent returnIntent) { @@ -248,6 +437,12 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene protected void onResume() { super.onResume(); + if (navigationView != null) { + updateCurrentKeyboardDrawerItemTitle(navigationView); + updateInstalledLanguagesDrawerTitle(navigationView); + initializeDrawerItemSubtitles(navigationView); + } + if (textView != null) { // Reset inAppPredictionsSuspendedForSensitiveInput flag KMManager.setPredictionsSuspended(textView.getInputType(), KeyboardType.KEYBOARD_TYPE_INAPP); @@ -366,31 +561,24 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene lastOrientation = newConfig.orientation; } - @SuppressLint("RestrictedApi") - @Override - public boolean onPrepareOptionsMenu(final Menu menu) { - this.menu = menu; - final MenuItem _overflowMenuItem = menu.findItem(R.id.action_overflow); - if (_overflowMenuItem != null) { - MenuItem updateKeyboards = this.menu.findItem(R.id.action_update_keyboards); - updateUpdateCountIndicator(updateKeyboards, - KMManager.getUpdateTool().getOpenUpdateCount(), true); - } - return super.onPrepareOptionsMenu(menu); - } - private void updateUpdateCountIndicator(int anUpdateCount) { if (menu == null) { return; } - final MenuItem _overflowMenuItem = menu.findItem(R.id.action_overflow); - if (_overflowMenuItem != null) { - updateUpdateCountIndicator(_overflowMenuItem, anUpdateCount, false); + // 1. Update the Top Action Bar Menu + if (menu != null) { + final MenuItem toolbarUpdate = menu.findItem(R.id.action_update_keyboards); + if (toolbarUpdate != null) { + toolbarUpdate.setVisible(anUpdateCount > 0); + } } - final MenuItem _keyboardupdate = menu.findItem(R.id.action_update_keyboards); - if (_keyboardupdate != null && anUpdateCount > 0) { - _keyboardupdate.setVisible(true); + // 2. Update the Drawer/Sidebar Navigation Menu + if (navigationView != null) { + final MenuItem drawerUpdate = navigationView.getMenu().findItem(R.id.action_update_keyboards); + if (drawerUpdate != null) { + drawerUpdate.setVisible(anUpdateCount > 0); + } } } @@ -432,8 +620,8 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene public void propertyChange(PropertyChangeEvent evt) { if(!evt.getPropertyName().equals("updateCount")) return; - updateUpdateCountIndicator( - evt.getNewValue()==null?0:(Integer) evt.getNewValue()); + int count = KMManager.getUpdateTool().getOpenUpdateCount(); + updateUpdateCountIndicator(count); } } ); @@ -470,11 +658,13 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene KMManager.getUpdateTool().executeOpenUpdates(); // Dismiss icon - we intentionally hide the icon, even if the user cancels the update updateUpdateCountIndicator(0); - final MenuItem _keyboardupdate = menu.findItem(R.id.action_update_keyboards); - if (_keyboardupdate != null && _keyboardupdate.isVisible()) { - _keyboardupdate.setVisible(false); + return true; + } else if (item.getItemId() == R.id.action_sidebar) { + if (drawerLayout.isDrawerOpen(GravityCompat.END)) { + drawerLayout.closeDrawer(GravityCompat.END); + } else { + drawerLayout.openDrawer(GravityCompat.END); } - return true; } else { return super.onOptionsItemSelected(item); @@ -485,13 +675,51 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene public boolean onKeyUp(int keycode, KeyEvent e) { switch (keycode) { case KeyEvent.KEYCODE_MENU: - menu.performIdentifierAction(R.id.action_overflow, Menu.FLAG_PERFORM_NO_CLOSE); + if (drawerLayout.isDrawerOpen(GravityCompat.END)) { + drawerLayout.closeDrawer(GravityCompat.END); + } else { + drawerLayout.openDrawer(GravityCompat.END); + } return true; } return super.onKeyUp(keycode, e); } + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + int action = ev.getActionMasked(); + // Collapse the keyboard toolbar when the user taps outside of it + if (action == MotionEvent.ACTION_DOWN && isKeyboardToolbarExpanded && !suppressOutsideCloseForCurrentTouch) { + boolean touchOnToggle = isTouchInsideView(ev, textPopupToggleButton); + if (touchOnToggle) { + return super.dispatchTouchEvent(ev); + } + + boolean touchOnToolbar = isTouchInsideView(ev, keyboardToolbarContainer); + if (!touchOnToolbar && !touchOnToggle) { + setKeyboardToolbarExpanded(false, true); + } + } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + suppressOutsideCloseForCurrentTouch = false; + } + return super.dispatchTouchEvent(ev); + } + + private boolean isTouchInsideView(MotionEvent ev, View view) { + if (view == null || view.getVisibility() != View.VISIBLE) { + return false; + } + + Rect bounds = new Rect(); + view.getGlobalVisibleRect(bounds); + return bounds.contains((int) ev.getRawX(), (int) ev.getRawY()); + } + private boolean isDarkMode() { + return (getResources().getConfiguration().uiMode & + Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + } + @Override public void onKeyboardLoaded(KeyboardType keyboardType) { // Do nothing @@ -499,25 +727,137 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene @Override public void onKeyboardChanged(String newKeyboard) { - textView.setTypeface(KMManager.getKeyboardTextFontTypeface(this)); + if (newKeyboard != null && !newKeyboard.isEmpty()) { + lastKnownKeyboardId = newKeyboard; + textView.setTypeface(KMManager.getKeyboardTextFontTypeface(this)); + } } @Override public void onKeyboardShown() { // Refresh banner theme - BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_INAPP); + boolean isDarkMode = isDarkMode(); + BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_INAPP, isDarkMode); resizeTextView(true); + updateKeyboardToolbarPosition(); + if (textPopupToggleButton != null) { + textPopupToggleButton.setVisibility(View.VISIBLE); + } } @Override public void onKeyboardDismissed() { resizeTextView(false); + setKeyboardToolbarExpanded(false, false); + if (textPopupToggleButton != null) { + textPopupToggleButton.setVisibility(View.GONE); + } + } + + private void initializeKeyboardToolbar() { + textPopupToggleButton = findViewById(R.id.keyboardToolbarToggleButton); + keyboardToolbarContainer = findViewById(R.id.keyboardToolbarContainer); + + if (textPopupToggleButton == null || keyboardToolbarContainer == null) { + reportKeyboardToolbarLayoutIssue("Keyboard toolbar layout is missing required views."); + return; + } + + setKeyboardToolbarExpanded(false, false); + textPopupToggleButton.setOnTouchListener((v, event) -> { + int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_DOWN) { + suppressOutsideCloseForCurrentTouch = true; + } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + suppressOutsideCloseForCurrentTouch = false; + } + return false; + }); + textPopupToggleButton.setOnClickListener(v -> setKeyboardToolbarExpanded(!isKeyboardToolbarExpanded, true)); + + View shareButton = findViewById(R.id.keyboardToolbarShareButton); + if (shareButton != null) { + shareButton.setOnClickListener(v -> { + showShareDialog(); + setKeyboardToolbarExpanded(false, true); + }); + } else { + reportKeyboardToolbarLayoutIssue("Keyboard toolbar share button is missing."); + } + + View textSizeButton = findViewById(R.id.keyboardToolbarTextSizeButton); + if (textSizeButton != null) { + textSizeButton.setOnClickListener(v -> { + showTextSizeDialog(); + setKeyboardToolbarExpanded(false, true); + }); + } else { + reportKeyboardToolbarLayoutIssue("Keyboard toolbar text size button is missing."); + } + + View clearButton = findViewById(R.id.keyboardToolbarClearButton); + if (clearButton != null) { + clearButton.setOnClickListener(v -> { + showClearTextDialog(); + setKeyboardToolbarExpanded(false, true); + }); + } else { + reportKeyboardToolbarLayoutIssue("Keyboard toolbar clear button is missing."); + } + } + + private void reportKeyboardToolbarLayoutIssue(String message) { + KMLog.LogError(TAG, message); + if (Sentry.isEnabled()) { + Sentry.captureMessage(message); + } + } + + private void setKeyboardToolbarExpanded(boolean expanded, boolean animateRotation) { + if (keyboardToolbarContainer == null || textPopupToggleButton == null) { + return; + } + + isKeyboardToolbarExpanded = expanded; + keyboardToolbarContainer.setVisibility(expanded ? View.VISIBLE : View.GONE); + + float targetRotation = expanded ? 0f : 180f; + if (animateRotation) { + textPopupToggleButton.animate().rotation(targetRotation).setDuration(180).start(); + } else { + textPopupToggleButton.setRotation(targetRotation); + } + } + + private void updateKeyboardToolbarPosition() { + int keyboardTopOffset = KMManager.getKeyboardHeight(this) + KMManager.getBannerHeight(this) + + getResources().getDimensionPixelSize(R.dimen.keyboard_toolbar_keyboard_gap); + updateBottomMargin(textPopupToggleButton, keyboardTopOffset); + updateBottomMargin(keyboardToolbarContainer, + getResources().getDimensionPixelSize(R.dimen.keyboard_toolbar_container_bottom_margin)); + } + + private void updateBottomMargin(View view, int bottomMargin) { + if (view == null) { + return; + } + + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + if (!(layoutParams instanceof ViewGroup.MarginLayoutParams)) { + return; + } + + ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) layoutParams; + if (marginLayoutParams.bottomMargin != bottomMargin) { + marginLayoutParams.bottomMargin = bottomMargin; + view.setLayoutParams(marginLayoutParams); + } } public void downloadKMP(String packageId, String bcp47, KmpInstallMode installMode) { Uri downloadUri = bcp47 == null ? - Uri.parse(KMString.format("https://keyman.com/go/package/download/%s", new Object[]{packageId})) : - Uri.parse(KMString.format("https://keyman.com/go/package/download/%s?bcp47=%s", new Object[]{packageId, bcp47})); + Uri.parse(KMString.format("https://keyman.com/go/package/download/%s", new Object[] { packageId })) : + Uri.parse(KMString.format("https://keyman.com/go/package/download/%s?bcp47=%s", new Object[] { packageId, bcp47 })); downloadKMP(downloadUri, installMode); } @@ -672,7 +1012,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene LayoutInflater inflater = LayoutInflater.from(MainActivity.this); final View textSizeController = inflater.inflate(R.layout.text_size_controller, null); final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(MainActivity.this); - dialogBuilder.setIcon(R.drawable.ic_light_action_textsize); + dialogBuilder.setIcon(R.drawable.ic_action_textsize_mode); dialogBuilder.setTitle(getTextSizeString()); dialogBuilder.setView(textSizeController); dialogBuilder.setPositiveButton(getString(R.string.label_ok), new DialogInterface.OnClickListener() { @@ -684,6 +1024,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene final AlertDialog dialog = dialogBuilder.create(); dialog.show(); + dialog.getWindow().setBackgroundDrawableResource(R.color.dialogBackground); final SeekBar seekBar = (SeekBar) dialog.findViewById(R.id.seekBar); seekBar.setProgress(textSize - minTextSize); @@ -741,7 +1082,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene private void showClearTextDialog() { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(MainActivity.this); - dialogBuilder.setIcon(R.drawable.ic_light_action_trash); + dialogBuilder.setIcon(R.drawable.ic_action_trash_mode); dialogBuilder.setTitle(getString(R.string.action_clear_text)); dialogBuilder.setMessage(getString(R.string.all_text_will_be_cleared)); dialogBuilder.setPositiveButton(getString(R.string.label_ok), new DialogInterface.OnClickListener() { @@ -758,7 +1099,9 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene } }); - dialogBuilder.show(); + final AlertDialog dialog = dialogBuilder.create(); + dialog.show(); + dialog.getWindow().setBackgroundDrawableResource(R.color.dialogBackground); } private void checkGetStarted() { @@ -783,13 +1126,13 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene private void checkSendCrashReport() { SharedPreferences prefs = getSharedPreferences(getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); - boolean maySendCrashReport = prefs.getBoolean(KeymanSettingsActivity.sendCrashReport, true); + boolean maySendCrashReport = prefs.getBoolean(KeymanSettingsKeys.SEND_CRASH_REPORT, true); KMManager.setMaySendCrashReport(maySendCrashReport); } private void checkHapticFeedback() { SharedPreferences prefs = getSharedPreferences(getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); - boolean mayHaveHapticFeedback = prefs.getBoolean(KeymanSettingsActivity.hapticFeedbackKey, false); + boolean mayHaveHapticFeedback = prefs.getBoolean(KeymanSettingsKeys.HAPTIC_FEEDBACK, false); KMManager.setHapticFeedback(mayHaveHapticFeedback); } @@ -966,8 +1309,393 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene } private void showSettings() { - Intent settingsIntent = new Intent(this, KeymanSettingsActivity.class); - startActivity(settingsIntent); + if (drawerLayout != null) { + drawerLayout.openDrawer(GravityCompat.END); + } + } + + private boolean handleDrawerNavigationItemSelected(int id) { + if (id == R.id.system_keyboards) { + startActivity(new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)); + } else if (id == R.id.action_update_keyboards) { + KMManager.getUpdateTool().executeOpenUpdates(); + updateUpdateCountIndicator(0); + } else if (id == R.id.action_settings) { + showSettings(); + } else if (id == R.id.nav_installed_languages) { + Intent intent = new Intent(context, LanguagesSettingsActivity.class); + intent.putExtra(KMManager.KMKey_DisplayKeyboardSwitcher, false); + startActivity(intent); + } else if (id == R.id.nav_install_keyboard) { + startActivity(new Intent(context, KeymanSettingsInstallActivity.class)); + } else if (id == R.id.nav_display_language) { + startActivity(new Intent(context, KeymanSettingsLocalizeActivity.class)); + } else if (id == R.id.action_get_started) { + startActivity(new Intent(context, GetStartedActivity.class)); + } else if (id == R.id.nav_keyboard_height) { + startActivity(new Intent(context, AdjustKeyboardHeightActivity.class)); + } else if (id == R.id.nav_longpress_delay) { + startActivity(new Intent(context, AdjustLongpressDelayActivity.class)); + } else if (id == R.id.nav_spacebar_caption) { + showSpacebarCaptionDialog(); + } else if (id == R.id.nav_help) { + startActivity(new Intent(context, InfoActivity.class)); + } else if (id == R.id.nav_palette) { + openThemeDialog(); + } else if (id == R.id.nav_about_current_keyboard) { + showCurrentKeyboardSettings(); + } + return true; + } + + private void showSpacebarCaptionDialog() { + CharSequence[] entries = { + getString(R.string.spacebar_caption_language), + getString(R.string.spacebar_caption_keyboard), + getString(R.string.spacebar_caption_language_keyboard), + getString(R.string.spacebar_caption_blank) + }; + + KMManager.SpacebarText[] values = { + KMManager.SpacebarText.LANGUAGE, + KMManager.SpacebarText.KEYBOARD, + KMManager.SpacebarText.LANGUAGE_KEYBOARD, + KMManager.SpacebarText.BLANK + }; + + KMManager.SpacebarText current = KMManager.getSpacebarText(); + int currentIndex = Arrays.asList(values).indexOf(current); + + new AlertDialog.Builder(context) + .setTitle(getString(R.string.spacebar_caption)) + .setSingleChoiceItems(entries, currentIndex, (dialog, which) -> { + KMManager.setSpacebarText(values[which]); + + SharedPreferences prefs = getSharedPreferences( + getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); + prefs.edit() + .putString(KeymanSettingsKeys.SPACEBAR_TEXT, values[which].toString()) + .apply(); + + dialog.dismiss(); + }) + .setNegativeButton(getString(R.string.label_cancel), null) + .show(); + } + + private void initializeDrawerToggleOptions(NavigationView navigationView) { + SharedPreferences prefs = getSharedPreferences(getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); + + bindDrawerSwitch(navigationView, R.id.nav_toggle_show_osk, + prefs.getBoolean(KeymanSettingsKeys.OSK_WITH_PHYSICAL_KEYBOARD, false), + new SwitchChangeHandler() { + @Override + public void onChanged(boolean isChecked) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(KeymanSettingsKeys.OSK_WITH_PHYSICAL_KEYBOARD, isChecked); + editor.apply(); + } + }); + + bindDrawerSwitch(navigationView, R.id.nav_toggle_send_crash_report, + prefs.getBoolean(KeymanSettingsKeys.SEND_CRASH_REPORT, true), + new SwitchChangeHandler() { + @Override + public void onChanged(boolean isChecked) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(KeymanSettingsKeys.SEND_CRASH_REPORT, isChecked); + editor.apply(); + KMManager.setMaySendCrashReport(isChecked); + } + }); + } + + private void initializeDrawerItemSubtitles(NavigationView navigationView) { + setDrawerItemSubtitle(navigationView, R.id.nav_spacebar_caption, + getString(R.string.drawer_subtitle_spacebar_caption)); + setDrawerItemSubtitle(navigationView, R.id.nav_toggle_show_osk, + getString(R.string.drawer_subtitle_show_osk)); + setDrawerItemSubtitle(navigationView, R.id.nav_toggle_send_crash_report, + getString(R.string.drawer_subtitle_send_crash_report)); + setDrawerItemSubtitle(navigationView, R.id.nav_about_current_keyboard, + getString(R.string.drawer_subtitle_about_current_keyboard)); + updateThemeDrawerSubtitle(navigationView); + updateCurrentKeyboardDrawerSubtitle(navigationView); + } + + private void updateCurrentKeyboardDrawerItemTitle(NavigationView navigationView) { + MenuItem menuItem = navigationView.getMenu().findItem(R.id.nav_about_current_keyboard); + if (menuItem == null) { + return; + } + + String newTitle = getString(R.string.drawer_about_current_keyboard); + + if (!newTitle.equals(lastCurrentKeyboardDrawerTitle)) { + menuItem.setTitle(newTitle); + lastCurrentKeyboardDrawerTitle = newTitle; + } + } + + private void updateCurrentKeyboardDrawerSubtitle(NavigationView navigationView) { + setDrawerItemSubtitle(navigationView, R.id.nav_about_current_keyboard, getCurrentKeyboardDisplayName()); + } + + private void updateThemeDrawerSubtitle(NavigationView navigationView) { + setDrawerItemSubtitle( + navigationView, + R.id.nav_palette, + getString(R.string.drawer_subtitle_theme, getThemeModeLabel()) + ); + } + + private void updateInstalledLanguagesDrawerTitle(NavigationView navigationView) { + MenuItem menuItem = navigationView.getMenu().findItem(R.id.nav_installed_languages); + if (menuItem == null) { + return; + } + + menuItem.setTitle(getInstalledLanguagesText()); + } + + private String getInstalledLanguagesText() { + Dataset installedDataset = KMManager.getInstalledDataset(context); + if (installedDataset == null) { + return getResources().getQuantityString(R.plurals.installed_languages, 0, 0); + } + + int langCount = 0; + for (Dataset.LanguageDataset language : installedDataset.asList()) { + if (language.keyboards.size() > 0) { + langCount++; + } + } + return getResources().getQuantityString(R.plurals.installed_languages, langCount, langCount); + } + + private String getCurrentKeyboardDisplayName() { + Keyboard currentKeyboard = getBestAvailableKeyboard(); + if (currentKeyboard != null && currentKeyboard.getKeyboardName() != null + && !currentKeyboard.getKeyboardName().isEmpty()) { + return currentKeyboard.getKeyboardName(); + } + return getString(R.string.drawer_about_no_active_keyboard); + } + + private String getThemeModeLabel() { + SharedPreferences themePrefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); + int currentMode = themePrefs.getInt(KEY_THEME_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + if (currentMode == AppCompatDelegate.MODE_NIGHT_YES) { + return getString(R.string.theme_dark_mode); + } + if (currentMode == AppCompatDelegate.MODE_NIGHT_NO) { + return getString(R.string.theme_light_mode); + } + return getString(R.string.theme_system_default); + } + + private void showCurrentKeyboardSettings() { + Keyboard currentKeyboard = getBestAvailableKeyboard(); + if (currentKeyboard == null) { + if (BuildConfig.DEBUG && DEBUG_CURRENT_KEYBOARD_LOGS) { + KMLog.LogInfo(TAG, "Current keyboard unavailable; not opening KeyboardSettingsActivity."); + } + if (SHOW_CURRENT_KEYBOARD_ERROR_TOAST) { + Toast.makeText(this, getString(R.string.drawer_about_no_active_keyboard), Toast.LENGTH_SHORT).show(); + } + return; + } + + Intent intent = new Intent(this, KeyboardSettingsActivity.class); + Bundle bundle = new Bundle(); + bundle.putSerializable(KMManager.KMKey_Keyboard, currentKeyboard); + intent.putExtras(bundle); + startActivity(intent); + } + + private Keyboard getBestAvailableKeyboard() { + List keyboards = KMManager.getKeyboardsList(this); + if (keyboards == null || keyboards.isEmpty()) { + if (BuildConfig.DEBUG && DEBUG_CURRENT_KEYBOARD_LOGS) { + KMLog.LogInfo(TAG, "No installed keyboards available."); + } + return null; + } + + if (keyboards.size() == 1) { + return keyboards.get(0); + } + + if (lastKnownKeyboardId != null && !lastKnownKeyboardId.isEmpty()) { + for (Keyboard keyboard : keyboards) { + String keyboardId = keyboard.getKeyboardID(); + if (keyboardId != null && (keyboardId.equals(lastKnownKeyboardId) + || lastKnownKeyboardId.contains(keyboardId) + || keyboardId.contains(lastKnownKeyboardId))) { + return keyboard; + } + } + } + + if (BuildConfig.DEBUG && DEBUG_CURRENT_KEYBOARD_LOGS) { + KMLog.LogInfo(TAG, "Current keyboard id unavailable; falling back to first installed keyboard."); + } + + return keyboards.get(0); + } + + private void setDrawerItemSubtitle(NavigationView navigationView, int menuItemId, String subtitle) { + MenuItem menuItem = navigationView.getMenu().findItem(menuItemId); + if (menuItem == null || subtitle == null || subtitle.isEmpty()) { + return; + } + + CharSequence currentTitle = menuItem.getTitle(); + if (currentTitle == null || currentTitle.length() == 0) { + return; + } + + String baseTitle = currentTitle.toString(); + int newlineIndex = baseTitle.indexOf('\n'); + if (newlineIndex >= 0) { + baseTitle = baseTitle.substring(0, newlineIndex); + } + + SpannableString fullTitle = new SpannableString(baseTitle + "\n" + subtitle); + int subtitleStart = baseTitle.length() + 1; + fullTitle.setSpan(new RelativeSizeSpan(0.85f), subtitleStart, fullTitle.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + TypedValue typedValue = new TypedValue(); + getTheme().resolveAttribute(android.R.attr.textColorSecondary, typedValue, true); + int subtitleColor = typedValue.resourceId != 0 + ? ContextCompat.getColor(this, typedValue.resourceId) + : typedValue.data; + fullTitle.setSpan(new ForegroundColorSpan(subtitleColor), subtitleStart, fullTitle.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + menuItem.setTitle(fullTitle); + } + + private void bindDrawerSwitch(NavigationView navigationView, int menuItemId, boolean defaultValue, + final SwitchChangeHandler switchChangeHandler) { + MenuItem menuItem = navigationView.getMenu().findItem(menuItemId); + if (menuItem == null) { + return; + } + + menuItem.setCheckable(false); + + View actionView = menuItem.getActionView(); + if (actionView == null) { + return; + } + + final SwitchCompat switchView; + if (actionView instanceof SwitchCompat) { + switchView = (SwitchCompat) actionView; + } else { + switchView = actionView.findViewById(R.id.nav_switch); + } + + if (switchView == null) { + return; + } + + switchView.setClickable(false); + switchView.setFocusable(false); + switchView.setLongClickable(false); + + switchView.setChecked(defaultValue); + switchView.setOnCheckedChangeListener((buttonView, isChecked) -> switchChangeHandler.onChanged(isChecked)); + } + + private void toggleDrawerSwitch(NavigationView navigationView, int menuItemId) { + MenuItem menuItem = navigationView.getMenu().findItem(menuItemId); + if (menuItem == null) { + return; + } + + View actionView = menuItem.getActionView(); + if (actionView == null) { + return; + } + + if (actionView instanceof SwitchCompat) { + ((SwitchCompat) actionView).toggle(); + return; + } + + SwitchCompat switchView = actionView.findViewById(R.id.nav_switch); + if (switchView != null) { + switchView.toggle(); + } + } + + private void bindDrawerCheckboxAction(NavigationView navigationView, int menuItemId, + final Runnable onActivate) { + MenuItem menuItem = navigationView.getMenu().findItem(menuItemId); + if (menuItem == null) { + return; + } + + menuItem.setCheckable(false); + + View actionView = menuItem.getActionView(); + if (actionView == null) { + return; + } + + final AppCompatCheckBox checkBox; + if (actionView instanceof AppCompatCheckBox) { + checkBox = (AppCompatCheckBox) actionView; + } else { + checkBox = actionView.findViewById(R.id.nav_checkbox); + } + + if (checkBox == null) { + return; + } + + checkBox.setClickable(true); + checkBox.setFocusable(false); + checkBox.setOnClickListener(v -> { + if (onActivate != null) { + onActivate.run(); + } + }); + actionView.setOnClickListener(v -> { + if (onActivate != null) { + onActivate.run(); + } + }); + } + + private void setDrawerCheckboxState(NavigationView navigationView, int menuItemId, boolean isChecked) { + MenuItem menuItem = navigationView.getMenu().findItem(menuItemId); + if (menuItem == null) { + return; + } + + View actionView = menuItem.getActionView(); + if (actionView == null) { + return; + } + + AppCompatCheckBox checkBox; + if (actionView instanceof AppCompatCheckBox) { + checkBox = (AppCompatCheckBox) actionView; + } else { + checkBox = actionView.findViewById(R.id.nav_checkbox); + } + + if (checkBox != null) { + checkBox.setChecked(isChecked); + } + } + + private interface SwitchChangeHandler { + void onChanged(boolean isChecked); } /** @@ -999,7 +1727,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene Paint p = new Paint(); p.setStyle(Paint.Style.FILL); - p.setColor(Color.WHITE); + p.setColor(ContextCompat.getColor(context, R.color.toolbarColor)); canvas.drawRect(new Rect(0, 0, width, actionBarHeight), p); p.setColor(context.getResources().getColor(R.color.keyman_orange)); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectLanguageFragment.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectLanguageFragment.java index 03d053e469..71cdfbe642 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectLanguageFragment.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectLanguageFragment.java @@ -6,7 +6,7 @@ package com.tavultesoft.kmapro; import android.content.Context; import android.graphics.Typeface; import android.os.Bundle; -import android.os.Handler; + import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -167,7 +167,7 @@ public final class SelectLanguageFragment extends Fragment implements BlockingSt if (!excludeInstalledLanguages && !isInstallingPackage && KeyboardController.getInstance().keyboardExists( k.getPackageID(), k.getKeyboardID(), k.getLanguageID())) { // If Activity is listing an installed keyboard package, mark installed keyboards with a check - icon = String.valueOf(R.drawable.ic_check); + icon = String.valueOf(R.drawable.ic_check_mode); enable = "false"; } else { // Update title diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectPackageActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectPackageActivity.java index 0b6a3ac9a7..c9bb86faca 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectPackageActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectPackageActivity.java @@ -83,7 +83,7 @@ public final class SelectPackageActivity extends BaseActivity { HashMap hashMap = new HashMap<>(); hashMap.put(titleKey, keyboardName); hashMap.put(subtitleKey, pkgID); - String icon = String.valueOf(R.drawable.ic_action_forward); + String icon = String.valueOf(R.drawable.ic_action_forward_mode); hashMap.put(iconKey, icon); list.add(hashMap); } diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-hdpi/keyman_ic.png b/android/KMAPro/kMAPro/src/main/res/drawable-hdpi/keyman_ic.png new file mode 100644 index 0000000000..cdd82a9eaf Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-hdpi/keyman_ic.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable-hdpi/keyman_text.png b/android/KMAPro/kMAPro/src/main/res/drawable-hdpi/keyman_text.png new file mode 100644 index 0000000000..48b555f559 Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable-hdpi/keyman_text.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/bg_keyboard_toolbar_container.xml b/android/KMAPro/kMAPro/src/main/res/drawable/bg_keyboard_toolbar_container.xml new file mode 100644 index 0000000000..1dfbf0c345 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/bg_keyboard_toolbar_container.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/bg_keyboard_toolbar_toggle.xml b/android/KMAPro/kMAPro/src/main/res/drawable/bg_keyboard_toolbar_toggle.xml new file mode 100644 index 0000000000..08f8c630b4 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/bg_keyboard_toolbar_toggle.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/branded_logo.xml b/android/KMAPro/kMAPro/src/main/res/drawable/branded_logo.xml index 2f5d1eca2b..23720f7c57 100644 --- a/android/KMAPro/kMAPro/src/main/res/drawable/branded_logo.xml +++ b/android/KMAPro/kMAPro/src/main/res/drawable/branded_logo.xml @@ -3,7 +3,7 @@ android:opacity="opaque"> - + - + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/branded_logo_dark_mode.xml b/android/KMAPro/kMAPro/src/main/res/drawable/branded_logo_dark_mode.xml new file mode 100644 index 0000000000..5cb427161f --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/branded_logo_dark_mode.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_decrement.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_decrement.xml index c3085bec5e..7f67a424a8 100644 --- a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_decrement.xml +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_decrement.xml @@ -1,5 +1,5 @@ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_forward_mode.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_forward_mode.xml new file mode 100644 index 0000000000..e50c72fd48 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_forward_mode.xml @@ -0,0 +1,4 @@ + + diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_get_started_mode.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_get_started_mode.xml new file mode 100644 index 0000000000..945b28f2f7 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_get_started_mode.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_increment.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_increment.xml index ea188f818f..04d80ab840 100644 --- a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_increment.xml +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_increment.xml @@ -1,5 +1,5 @@ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_info_mode.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_info_mode.xml new file mode 100644 index 0000000000..cde024c5df --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_info_mode.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_overflow_mode.png b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_overflow_mode.png new file mode 100644 index 0000000000..b78c32e85f Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_overflow_mode.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_overflow_mode_small.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_overflow_mode_small.xml new file mode 100644 index 0000000000..b9581eec2c --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_overflow_mode_small.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_overflow_mode_tinted.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_overflow_mode_tinted.xml new file mode 100644 index 0000000000..7df2a0f1a5 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_overflow_mode_tinted.xml @@ -0,0 +1,4 @@ + + diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_share_mode.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_share_mode.xml new file mode 100644 index 0000000000..094bccaf5f --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_share_mode.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_textsize_mode.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_textsize_mode.xml new file mode 100644 index 0000000000..c9bce1b644 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_textsize_mode.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_timelapse.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_timelapse.xml index 80dc29e33f..c185c3bd11 100644 --- a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_timelapse.xml +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_timelapse.xml @@ -2,4 +2,4 @@ - + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_trash_mode.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_trash_mode.xml new file mode 100644 index 0000000000..7ce6e6f02e --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_action_trash_mode.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_arrow_dropdown.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_arrow_dropdown.xml new file mode 100644 index 0000000000..38b4ddbe58 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_arrow_dropdown.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_check_mode.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_check_mode.xml new file mode 100644 index 0000000000..45126f7b91 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_check_mode.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_checkbox.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_checkbox.xml new file mode 100644 index 0000000000..bd790eec59 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_checkbox.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_cloud_download_mode.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_cloud_download_mode.xml new file mode 100644 index 0000000000..dd95be3da2 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_cloud_download_mode.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_content_add_mode.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_content_add_mode.xml new file mode 100644 index 0000000000..9f14c6d6db --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_content_add_mode.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_folder_open_mode.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_folder_open_mode.xml new file mode 100644 index 0000000000..00896d44cb --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_folder_open_mode.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_keyboard_default.png b/android/KMAPro/kMAPro/src/main/res/drawable/ic_keyboard_default.png new file mode 100644 index 0000000000..1f08b2e9e3 Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable/ic_keyboard_default.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_keyboard_on_screen.png b/android/KMAPro/kMAPro/src/main/res/drawable/ic_keyboard_on_screen.png new file mode 100644 index 0000000000..c8707e943d Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable/ic_keyboard_on_screen.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_keyboard_physical.png b/android/KMAPro/kMAPro/src/main/res/drawable/ic_keyboard_physical.png new file mode 100644 index 0000000000..4ef7ec9fe1 Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable/ic_keyboard_physical.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_more_vert_24.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_more_vert_24.xml new file mode 100644 index 0000000000..f79e4779fa --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_more_vert_24.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_palette.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_palette.xml new file mode 100644 index 0000000000..f701027ccf --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_palette.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_rocket.png b/android/KMAPro/kMAPro/src/main/res/drawable/ic_rocket.png new file mode 100644 index 0000000000..695eaac970 Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable/ic_rocket.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_settings_mode.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_settings_mode.xml new file mode 100644 index 0000000000..63dacb5436 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_settings_mode.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/ic_warning.xml b/android/KMAPro/kMAPro/src/main/res/drawable/ic_warning.xml new file mode 100644 index 0000000000..7b9ff21539 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/ic_warning.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/keyman_logo_mode.xml b/android/KMAPro/kMAPro/src/main/res/drawable/keyman_logo_mode.xml new file mode 100644 index 0000000000..63182c6c53 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/keyman_logo_mode.xml @@ -0,0 +1,21 @@ + + + + + + + + + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/keyman_logo_splash_dark_mode.png b/android/KMAPro/kMAPro/src/main/res/drawable/keyman_logo_splash_dark_mode.png new file mode 100644 index 0000000000..0ed8dd4e0d Binary files /dev/null and b/android/KMAPro/kMAPro/src/main/res/drawable/keyman_logo_splash_dark_mode.png differ diff --git a/android/KMAPro/kMAPro/src/main/res/drawable/spacebar_key_bg.xml b/android/KMAPro/kMAPro/src/main/res/drawable/spacebar_key_bg.xml new file mode 100644 index 0000000000..0b55d9e593 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/res/drawable/spacebar_key_bg.xml @@ -0,0 +1,17 @@ + + + + + + + + + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/layout/activity_adjust_keyboard_height.xml b/android/KMAPro/kMAPro/src/main/res/layout/activity_adjust_keyboard_height.xml index 8541398a57..4c43d82269 100644 --- a/android/KMAPro/kMAPro/src/main/res/layout/activity_adjust_keyboard_height.xml +++ b/android/KMAPro/kMAPro/src/main/res/layout/activity_adjust_keyboard_height.xml @@ -7,7 +7,11 @@ android:layout_height="match_parent" android:orientation="vertical"> - + + app:tint="@color/iconColor" + /> + app:layout_constraintEnd_toEndOf="parent" + app:tint="@color/iconColor"/> diff --git a/android/KMAPro/kMAPro/src/main/res/layout/activity_main.xml b/android/KMAPro/kMAPro/src/main/res/layout/activity_main.xml index ffcd4a2421..7c3e004b0d 100644 --- a/android/KMAPro/kMAPro/src/main/res/layout/activity_main.xml +++ b/android/KMAPro/kMAPro/src/main/res/layout/activity_main.xml @@ -1,42 +1,127 @@ - + android:fitsSystemWindows="true"> - - - + android:layout_height="match_parent" + android:layout_above="@+id/KMKeyboard" + android:orientation="vertical" + android:background="@color/toolbarColor" + tools:context=".MainActivity" + android:fitsSystemWindows="true"> - + + + + + + + + + + + + + + + + + + + + + + + + android:fadeScrollbars="false" + android:fitsSystemWindows="true" + app:itemTextAppearance="@style/NavigationDrawerItemText" + app:menu="@menu/nav_menu" /> - - - - + diff --git a/android/KMAPro/kMAPro/src/main/res/layout/activity_splash_screen.xml b/android/KMAPro/kMAPro/src/main/res/layout/activity_splash_screen.xml index c6cde0e259..128cadc6e1 100644 --- a/android/KMAPro/kMAPro/src/main/res/layout/activity_splash_screen.xml +++ b/android/KMAPro/kMAPro/src/main/res/layout/activity_splash_screen.xml @@ -1,19 +1,18 @@ - - + \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/res/layout/check_chrome_webview_layout.xml b/android/KMAPro/kMAPro/src/main/res/layout/check_chrome_webview_layout.xml index ea5a9220d5..7ceb1bad59 100644 --- a/android/KMAPro/kMAPro/src/main/res/layout/check_chrome_webview_layout.xml +++ b/android/KMAPro/kMAPro/src/main/res/layout/check_chrome_webview_layout.xml @@ -3,20 +3,23 @@ android:id="@+id/checkWebViewChromeLayout" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@android:color/white" + android:paddingVertical="4dp" + android:background="?android:attr/windowBackground" + android:gravity="center_vertical" android:orientation="horizontal">