diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 95d80b9bd..77d4f7513 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -31,6 +31,12 @@ + + + + + + diff --git a/assets/special_font.ttf b/assets/special_font.ttf index b124fc553..6c7e38c12 100644 Binary files a/assets/special_font.ttf and b/assets/special_font.ttf differ diff --git a/res/layout/dictionary_picker_item.xml b/res/layout/dictionary_picker_item.xml new file mode 100644 index 000000000..c9afd464a --- /dev/null +++ b/res/layout/dictionary_picker_item.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/res/layout/dictionary_picker_pane.xml b/res/layout/dictionary_picker_pane.xml new file mode 100644 index 000000000..1b25df89c --- /dev/null +++ b/res/layout/dictionary_picker_pane.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/layout/keyboard.xml b/res/layout/keyboard.xml index 712c48a40..c1a5a413a 100644 --- a/res/layout/keyboard.xml +++ b/res/layout/keyboard.xml @@ -5,6 +5,7 @@ + diff --git a/res/layout/personal_dict_activity.xml b/res/layout/personal_dict_activity.xml new file mode 100644 index 000000000..41577ed47 --- /dev/null +++ b/res/layout/personal_dict_activity.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/res/layout/personal_dict_entry.xml b/res/layout/personal_dict_entry.xml new file mode 100644 index 000000000..68b831233 --- /dev/null +++ b/res/layout/personal_dict_entry.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/res/menu/personal_dict_menu.xml b/res/menu/personal_dict_menu.xml new file mode 100644 index 000000000..51f111f7e --- /dev/null +++ b/res/menu/personal_dict_menu.xml @@ -0,0 +1,6 @@ + + + diff --git a/res/values/layouts.xml b/res/values/layouts.xml index 11ac6375e..91d23d96d 100644 --- a/res/values/layouts.xml +++ b/res/values/layouts.xml @@ -47,6 +47,8 @@ latn_azerty_fr latn_bepo_fr latn_bone + latn_dvorak_landscape + latn_dvorak_left latn_neo2 latn_qwerty_apl latn_qwerty_az @@ -141,6 +143,8 @@ AZERTY (Français) BEPO (Français) Bone + Dvorak-Landscape + Dvorak-Left Neo 2 QWERTY (APL) QWERTY (Azərbaycanca) @@ -235,6 +239,8 @@ @xml/latn_azerty_fr @xml/latn_bepo_fr @xml/latn_bone + @xml/latn_dvorak_landscape + @xml/latn_dvorak_left @xml/latn_neo2 @xml/latn_qwerty_apl @xml/latn_qwerty_az diff --git a/res/values/strings.xml b/res/values/strings.xml index 57b88f377..465e43e7e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -161,5 +161,27 @@ Dictionary installed Download failed You must first enable the keyboard to download dictionaries. + No dictionary installed + Install Click to install a dictionary for %s + + + Add word to personal dictionary + Active dictionary + Auto (based on language) + Select dictionary + Auto-correct with space bar + Manage dictionaries + Download or remove language dictionaries + Personal dictionary + Custom words and autocorrections + Personal dictionary + Add word + Add word + Edit word + Word (trigger) + Replacement (leave blank to use word as-is) + Save + Delete + Word cannot be empty + No words added yet. Tap \"Add word\" or use the + button while typing. diff --git a/res/xml/dict_picker_bottom_row.xml b/res/xml/dict_picker_bottom_row.xml new file mode 100644 index 000000000..ce1251fda --- /dev/null +++ b/res/xml/dict_picker_bottom_row.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/xml/settings.xml b/res/xml/settings.xml index d846dd1f1..16e0de463 100644 --- a/res/xml/settings.xml +++ b/res/xml/settings.xml @@ -15,6 +15,9 @@ + + + diff --git a/srcs/juloo.keyboard2/Config.java b/srcs/juloo.keyboard2/Config.java index a09e10fff..b249c576b 100644 --- a/srcs/juloo.keyboard2/Config.java +++ b/srcs/juloo.keyboard2/Config.java @@ -10,6 +10,7 @@ import java.util.Map; import juloo.cdict.Cdict; import juloo.keyboard2.dict.Dictionaries; +import juloo.keyboard2.dict.PersonalDictionary; import juloo.keyboard2.prefs.CustomExtraKeysPreference; import juloo.keyboard2.prefs.ExtraKeysPreference; import juloo.keyboard2.prefs.LayoutsPreference; @@ -85,6 +86,9 @@ public final class Config public Map extra_keys_custom; public DeviceLocales device_locales = null; public Cdict current_dictionary = null; // Might be 'null'. + public PersonalDictionary personal_dictionary = null; // Might be 'null'. + /** "auto" means use the locale-based dictionary. */ + public String selected_dictionary; public Cdict emoji_dictionary = null; // Might be 'null'. public IKeyEventHandler handler; public boolean orientation_landscape = false; @@ -195,6 +199,7 @@ public void refresh(Resources res, Boolean foldableUnfolded, Dictionaries dicts) clipboard_history_enabled = _prefs.getBoolean("clipboard_history_enabled", false); clipboard_history_duration = Integer.parseInt(_prefs.getString("clipboard_history_duration", "5")); space_bar_auto_complete = _prefs.getBoolean("space_bar_auto_complete", false); + selected_dictionary = _prefs.getString("selected_dictionary", "auto"); float screen_width_dp = dm.widthPixels / dm.density; wide_screen = screen_width_dp >= WIDE_DEVICE_THRESHOLD; diff --git a/srcs/juloo.keyboard2/KeyEventHandler.java b/srcs/juloo.keyboard2/KeyEventHandler.java index 78a4095ea..9bd3b20ce 100644 --- a/srcs/juloo.keyboard2/KeyEventHandler.java +++ b/srcs/juloo.keyboard2/KeyEventHandler.java @@ -137,6 +137,7 @@ public void suggestion_entered(String text) int cur_rel = _typedword.cursor_relative(); replace_surrounding_text(old.length() + cur_rel, -cur_rel, text + " "); last_replaced_word = old; + last_correction_word = text; last_replacement_word_len = text.length() + 1; _next_last_action = LastAction.SUGGESTION_ENTERED; } @@ -159,6 +160,11 @@ public void ime_subtype_changed() _suggestions.currently_typed_word(_typedword.get()); } + public void set_space_bar_auto_complete(boolean enabled) + { + _space_bar_auto_complete = enabled; + } + /** Update [_mods] to be consistent with the [mods], sending key events if needed. */ void update_meta_state(Pointers.Modifiers mods) @@ -525,6 +531,8 @@ void cancel_selection() enter a suggestion (with the space bar or the candidates view) or [null] otherwise. */ String last_replaced_word = null; + /** The suggestion that replaced [last_replaced_word]. */ + String last_correction_word = null; /** Length of the text before the cursor that should be replaced by backspace. */ int last_replacement_word_len = 0; @@ -548,7 +556,11 @@ void handle_backspace() { replace_surrounding_text(last_replacement_word_len, 0, last_replaced_word + " "); + String original = last_replaced_word; + String corrected = last_correction_word; last_replaced_word = null; + last_correction_word = null; + _recv.on_autocorrect_undone(original, corrected); } else { @@ -564,6 +576,9 @@ public static interface IReceiver extends Suggestions.Callback public void selection_state_changed(boolean selection_is_ongoing); public InputConnection getCurrentInputConnection(); public Handler getHandler(); + /** Called after backspace undoes an autocorrect. [original] is the word + the user typed, [corrected] is what autocorrect inserted. */ + public void on_autocorrect_undone(String original, String corrected); } class Autocapitalisation_callback implements Autocapitalisation.Callback diff --git a/srcs/juloo.keyboard2/KeyValue.java b/srcs/juloo.keyboard2/KeyValue.java index 04f695070..3f31e6423 100644 --- a/srcs/juloo.keyboard2/KeyValue.java +++ b/srcs/juloo.keyboard2/KeyValue.java @@ -24,6 +24,8 @@ public static enum Event CAPS_LOCK, SWITCH_VOICE_TYPING, SWITCH_VOICE_TYPING_CHOOSER, + SWITCH_DICTIONARY, + SWITCH_BACK_DICT_PICKER, } // Must be evaluated in the reverse order of their values. @@ -643,6 +645,8 @@ public static KeyValue getSpecialKeyByName(String name) case "capslock": return eventKey(0xE012, Event.CAPS_LOCK, 0); case "voice_typing": return eventKey(0xE015, Event.SWITCH_VOICE_TYPING, FLAG_SMALLER_FONT); case "voice_typing_chooser": return VOICE_TYPING_CHOOSER; + case "switch_dictionary": return eventKey(0xE01D, Event.SWITCH_DICTIONARY, FLAG_KEY_FONT | FLAG_SMALLER_FONT); + case "switch_back_dict_picker": return eventKey("ABC", Event.SWITCH_BACK_DICT_PICKER, 0); /* Key events */ case "esc": return keyeventKey("Esc", KeyEvent.KEYCODE_ESCAPE, FLAG_SMALLER_FONT); diff --git a/srcs/juloo.keyboard2/Keyboard2.java b/srcs/juloo.keyboard2/Keyboard2.java index 01d6f1684..f7e5b4664 100644 --- a/srcs/juloo.keyboard2/Keyboard2.java +++ b/srcs/juloo.keyboard2/Keyboard2.java @@ -21,6 +21,7 @@ import android.widget.LinearLayout; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; +import android.view.Gravity; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -28,6 +29,9 @@ import juloo.cdict.Cdict; import juloo.keyboard2.dict.Dictionaries; import juloo.keyboard2.dict.DictionariesActivity; +import juloo.keyboard2.dict.PersonalDictionary; +import juloo.keyboard2.dict.PersonalDictionaryActivity; +import juloo.keyboard2.dict.SupportedDictionaries; import juloo.keyboard2.prefs.LayoutsPreference; import juloo.keyboard2.suggestions.CandidatesView; import juloo.keyboard2.suggestions.Suggestions; @@ -46,8 +50,10 @@ public class Keyboard2 extends InputMethodService private KeyboardData _localeTextLayout; /** Installed and current locales. */ private Dictionaries _dictionaries; + private PersonalDictionary _personal_dictionary; private ViewGroup _emojiPane = null; private ViewGroup _clipboard_pane = null; + private ViewGroup _dictionary_picker_pane = null; private Handler _handler; private Config _config; @@ -123,9 +129,11 @@ public void onCreate() _handler = new Handler(getMainLooper()); _foldStateTracker = new FoldStateTracker(this); _dictionaries = Dictionaries.instance(this); + _personal_dictionary = PersonalDictionary.instance(this); Config.initGlobalConfig(prefs, getResources(), _foldStateTracker.isUnfolded(), _dictionaries); _config = Config.globalConfig(); + _config.personal_dictionary = _personal_dictionary; _keyeventhandler = new KeyEventHandler(this.new Receiver(), _config); _config.handler = _keyeventhandler; prefs.registerOnSharedPreferenceChangeListener(this); @@ -176,12 +184,21 @@ private void refresh_current_dictionary() { _config.current_dictionary = null; _config.emoji_dictionary = null; - if (_config.device_locales.default_ == null) - return; - String current = _config.device_locales.default_.dictionary; - if (current == null) - return; - Cdict[] dicts = _dictionaries.load(current); + String selected = _config.selected_dictionary; + String dict_name; + if (selected == null || selected.equals("auto")) + { + if (_config.device_locales.default_ == null) + return; + dict_name = _config.device_locales.default_.dictionary; + if (dict_name == null) + return; + } + else + { + dict_name = selected; + } + Cdict[] dicts = _dictionaries.load(dict_name); if (dicts == null) return; _config.current_dictionary = Dictionaries.find_by_name(dicts, "main"); @@ -211,6 +228,7 @@ private void refresh_config() create_keyboard_view(); _emojiPane = null; _clipboard_pane = null; + _dictionary_picker_pane = null; setInputView(_keyboard_container_view); } // Set keyboard background opacity @@ -382,6 +400,12 @@ public void launch_dictionaries_activity(View v) start_activity(DictionariesActivity.class); } + /** Called from [onClick] attributes. */ + public void launch_personal_dictionary_activity(View v) + { + start_activity(PersonalDictionaryActivity.class); + } + void start_activity(Class cls) { Intent intent = new Intent(this, cls); @@ -421,6 +445,36 @@ public void handle_event_key(KeyValue.Event ev) setInputView(_clipboard_pane); break; + case SWITCH_DICTIONARY: + _dictionary_picker_pane = (ViewGroup)inflate_view(R.layout.dictionary_picker_pane); + build_dictionary_picker(_dictionary_picker_pane); + final int dict_picker_h = + _keyboard_layout_view.getMeasuredHeight() + _candidates_view.getMeasuredHeight(); + setInputView(_dictionary_picker_pane); + if (dict_picker_h > 0) + { + final ViewGroup picker_pane = _dictionary_picker_pane; + picker_pane.getViewTreeObserver().addOnPreDrawListener( + new android.view.ViewTreeObserver.OnPreDrawListener() + { + @Override + public boolean onPreDraw() + { + picker_pane.getViewTreeObserver().removeOnPreDrawListener(this); + android.view.ViewGroup.LayoutParams lp = picker_pane.getLayoutParams(); + if (lp != null && lp.height != dict_picker_h) + { + lp.height = dict_picker_h; + picker_pane.setLayoutParams(lp); + return false; + } + return true; + } + }); + } + break; + + case SWITCH_BACK_DICT_PICKER: case SWITCH_BACK_EMOJI: case SWITCH_BACK_CLIPBOARD: setInputView(_keyboard_container_view); @@ -508,6 +562,16 @@ public void set_suggestions(Suggestions suggestions) { _candidates_view.set_candidates(suggestions); } + + public void set_current_word(String word) + { + _candidates_view.set_current_word(word); + } + + public void on_autocorrect_undone(String original, String corrected) + { + _candidates_view.on_autocorrect_undone(original, corrected); + } } private IBinder getConnectionToken() @@ -515,6 +579,86 @@ private IBinder getConnectionToken() return getWindow().getWindow().getAttributes().token; } + /** Populate the dictionary picker pane with the current set of installed + dictionaries and wire up selection callbacks. */ + private void build_dictionary_picker(ViewGroup pane) + { + android.widget.Switch toggle = + (android.widget.Switch)pane.findViewById(R.id.dict_picker_autocorrect_switch); + toggle.setChecked(_config.space_bar_auto_complete); + toggle.setOnCheckedChangeListener(new android.widget.CompoundButton.OnCheckedChangeListener() + { + @Override + public void onCheckedChanged(android.widget.CompoundButton btn, boolean checked) + { + Config.globalPrefs().edit() + .putBoolean("space_bar_auto_complete", checked) + .apply(); + _config.space_bar_auto_complete = checked; + _keyeventhandler.set_space_bar_auto_complete(checked); + } + }); + + View abc_btn = pane.findViewById(R.id.dict_picker_abc_btn); + if (abc_btn != null) + abc_btn.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View v) + { + setInputView(_keyboard_container_view); + } + }); + + android.widget.LinearLayout list = + (android.widget.LinearLayout)pane.findViewById(R.id.dict_picker_list); + list.removeAllViews(); + SupportedDictionaries supported = new SupportedDictionaries(getResources()); + Set installed = _dictionaries.get_installed(); + String current = _config.selected_dictionary; + + add_dictionary_picker_item(list, getString(R.string.pref_selected_dictionary_auto), + "auto", "auto".equals(current) || current == null); + + for (int i = 0; i < supported.length(); i++) + { + final String name = supported.dict_name(i); + if (!installed.contains(name)) + continue; + add_dictionary_picker_item(list, supported.display_name(i), + name, name.equals(current)); + } + } + + private void add_dictionary_picker_item(android.widget.LinearLayout list, + String label, String dict_key, boolean is_current) + { + android.view.View row = + View.inflate(new ContextThemeWrapper(this, _config.theme), + R.layout.dictionary_picker_item, null); + ((android.widget.TextView)row.findViewById(R.id.dict_picker_item_name)) + .setText(label); + android.widget.TextView check = + (android.widget.TextView)row.findViewById(R.id.dict_picker_item_check); + check.setVisibility(is_current ? View.VISIBLE : View.GONE); + + row.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View v) + { + Config.globalPrefs().edit() + .putString("selected_dictionary", dict_key) + .apply(); + _config.selected_dictionary = dict_key; + refresh_current_dictionary(); + _keyeventhandler.ime_subtype_changed(); + setInputView(_keyboard_container_view); + } + }); + list.addView(row); + } + private View inflate_view(int layout) { return View.inflate(new ContextThemeWrapper(this, _config.theme), layout, null); diff --git a/srcs/juloo.keyboard2/SettingsActivity.java b/srcs/juloo.keyboard2/SettingsActivity.java index dffc986dc..d40643b28 100644 --- a/srcs/juloo.keyboard2/SettingsActivity.java +++ b/srcs/juloo.keyboard2/SettingsActivity.java @@ -1,11 +1,15 @@ package juloo.keyboard2; +import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; +import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.PreferenceManager; +import juloo.keyboard2.dict.DictionariesActivity; +import juloo.keyboard2.dict.PersonalDictionaryActivity; public class SettingsActivity extends PreferenceActivity { @@ -24,6 +28,9 @@ public void onCreate(Bundle savedInstanceState) catch (Exception _e) { fallbackEncrypted(); return; } addPreferencesFromResource(R.xml.settings); + wire_activity_pref("pref_manage_dictionaries", DictionariesActivity.class); + wire_activity_pref("pref_personal_dictionary", PersonalDictionaryActivity.class); + boolean foldableDevice = FoldStateTracker.isFoldableDevice(this); findPreference("margin_bottom_portrait_unfolded").setEnabled(foldableDevice); findPreference("margin_bottom_landscape_unfolded").setEnabled(foldableDevice); @@ -33,6 +40,19 @@ public void onCreate(Bundle savedInstanceState) findPreference("keyboard_height_landscape_unfolded").setEnabled(foldableDevice); } + void wire_activity_pref(String key, final Class activity_class) + { + findPreference(key).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() + { + @Override + public boolean onPreferenceClick(Preference preference) + { + startActivity(new Intent(SettingsActivity.this, activity_class)); + return true; + } + }); + } + void fallbackEncrypted() { // Can't communicate with the user here. diff --git a/srcs/juloo.keyboard2/dict/DictionaryListView.java b/srcs/juloo.keyboard2/dict/DictionaryListView.java index 9e6130593..3baeff57e 100644 --- a/srcs/juloo.keyboard2/dict/DictionaryListView.java +++ b/srcs/juloo.keyboard2/dict/DictionaryListView.java @@ -17,8 +17,6 @@ import java.util.Set; import java.util.zip.GZIPInputStream; import juloo.cdict.Cdict; -import juloo.keyboard2.Config; -import juloo.keyboard2.DeviceLocales; import juloo.keyboard2.Logs; import juloo.keyboard2.R; import juloo.keyboard2.Utils; @@ -39,24 +37,16 @@ public DictionaryListView(Context ctx, AttributeSet attrs) void inflate_views(Context ctx) { - DeviceLocales locales = DeviceLocales.load(ctx); SupportedDictionaries ds = new SupportedDictionaries(ctx.getResources()); DownloadBtnListener listener = this.new DownloadBtnListener(); _dict_views = new ArrayList(); - for (DeviceLocales.Loc loc : locales.installed) + for (int i = 0; i < ds.length(); i++) { - int idx = (loc.dictionary != null) ? ds.find(loc.dictionary) : -1; - if (idx >= 0) - { - DictView dv = new DictView(ctx, ds, idx, listener); - addView(dv.view); - _dict_views.add(dv); - } + DictView dv = new DictView(ctx, ds, i, listener); + addView(dv.view); + _dict_views.add(dv); } refresh(); - // The keyboard is not enabled and the list is empty, show a message. - if (locales.installed.size() == 0) - addView(View.inflate(ctx, R.layout.dictionary_status_not_enabled, null)); } /** Update the "installed" status of item views. Meaning whether the diff --git a/srcs/juloo.keyboard2/dict/PersonalDictionary.java b/srcs/juloo.keyboard2/dict/PersonalDictionary.java new file mode 100644 index 000000000..79e51a60c --- /dev/null +++ b/srcs/juloo.keyboard2/dict/PersonalDictionary.java @@ -0,0 +1,128 @@ +package juloo.keyboard2.dict; + +import android.content.Context; +import android.content.SharedPreferences; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** Manages user-defined words and custom autocorrections. */ +public final class PersonalDictionary +{ + static final char SEPARATOR = '\0'; + static final String PREF_NAME = "personal_dictionary"; + static final String PREF_WORDS = "words"; + + public static final class Entry + { + public final String word; // trigger / word to recognise + public final String replacement; // what to insert (== word if no autocorrect) + + Entry(String word, String replacement) + { + this.word = word; + this.replacement = (replacement != null && !replacement.isEmpty()) ? replacement : word; + } + + static Entry parse(String raw) + { + int sep = raw.indexOf(SEPARATOR); + if (sep < 0) + return new Entry(raw, raw); + return new Entry(raw.substring(0, sep), raw.substring(sep + 1)); + } + + String serialize() + { + if (word.equals(replacement)) + return word; + return word + SEPARATOR + replacement; + } + } + + static PersonalDictionary _instance = null; + + public static PersonalDictionary instance(Context ctx) + { + if (_instance == null) + _instance = new PersonalDictionary(ctx.getApplicationContext()); + return _instance; + } + + SharedPreferences _prefs; + List _entries; + + PersonalDictionary(Context ctx) + { + _prefs = ctx.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + load(); + } + + void load() + { + Set raw = _prefs.getStringSet(PREF_WORDS, null); + _entries = new ArrayList<>(); + if (raw != null) + { + for (String s : raw) + _entries.add(Entry.parse(s)); + } + } + + void save() + { + Set raw = new HashSet<>(); + for (Entry e : _entries) + raw.add(e.serialize()); + _prefs.edit().putStringSet(PREF_WORDS, raw).apply(); + } + + public List get_all() { return _entries; } + + /** Add (or update) an entry. [replacement] may be null/empty to mean the + word stands on its own without autocorrect. */ + public void add(String word, String replacement) + { + for (int i = _entries.size() - 1; i >= 0; i--) + if (_entries.get(i).word.equalsIgnoreCase(word)) + _entries.remove(i); + _entries.add(new Entry(word, (replacement != null && !replacement.isEmpty()) ? replacement : word)); + save(); + } + + public void remove(String word) + { + for (int i = _entries.size() - 1; i >= 0; i--) + if (_entries.get(i).word.equalsIgnoreCase(word)) + _entries.remove(i); + save(); + } + + /** Fill [dst] starting at [offset] with up to [max_count] suggestions. + Returns the number of entries added. */ + public int find_suggestions(String typed_word, String[] dst, int offset, int max_count) + { + if (_entries.isEmpty() || typed_word.length() < 1) + return 0; + String lower = typed_word.toLowerCase(); + int count = 0; + // Exact word match first (supports autocorrect replacements) + for (Entry e : _entries) + { + if (count >= max_count || offset + count >= dst.length) + break; + if (e.word.equalsIgnoreCase(typed_word)) + dst[offset + count++] = e.replacement; + } + // Prefix match next + for (Entry e : _entries) + { + if (count >= max_count || offset + count >= dst.length) + break; + if (!e.word.equalsIgnoreCase(typed_word) && e.word.toLowerCase().startsWith(lower)) + dst[offset + count++] = e.replacement; + } + return count; + } +} diff --git a/srcs/juloo.keyboard2/dict/PersonalDictionaryActivity.java b/srcs/juloo.keyboard2/dict/PersonalDictionaryActivity.java new file mode 100644 index 000000000..99416da88 --- /dev/null +++ b/srcs/juloo.keyboard2/dict/PersonalDictionaryActivity.java @@ -0,0 +1,202 @@ +package juloo.keyboard2.dict; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; +import java.util.List; +import juloo.keyboard2.R; + +public class PersonalDictionaryActivity extends Activity +{ + public static final String EXTRA_WORD = "word"; + public static final String EXTRA_REPLACEMENT = "replacement"; + + PersonalDictionary _dict; + LinearLayout _list_container; + + @Override + public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + _dict = PersonalDictionary.instance(this); + setContentView(R.layout.personal_dict_activity); + _list_container = (LinearLayout)findViewById(R.id.personal_dict_list); + setTitle(R.string.personal_dict_title); + if (getActionBar() != null) + getActionBar().setDisplayHomeAsUpEnabled(true); + + String prefill_word = getIntent().getStringExtra(EXTRA_WORD); + String prefill_replacement = getIntent().getStringExtra(EXTRA_REPLACEMENT); + if (prefill_word != null && !prefill_word.isEmpty()) + show_add_dialog(prefill_word, prefill_replacement != null ? prefill_replacement : ""); + + refresh_list(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) + { + getMenuInflater().inflate(R.menu.personal_dict_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + int id = item.getItemId(); + if (id == R.id.personal_dict_menu_add) + { + show_add_dialog(null, null); + return true; + } + if (id == android.R.id.home) + { + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + void show_add_dialog(String initial_word, String initial_replacement) + { + show_word_dialog( + getString(R.string.personal_dict_add_title), + initial_word != null ? initial_word : "", + initial_replacement != null ? initial_replacement : "", + new OnSaveListener() + { + @Override + public void on_save(String word, String replacement) + { + _dict.add(word, replacement); + refresh_list(); + } + }); + } + + void show_edit_dialog(PersonalDictionary.Entry entry) + { + boolean has_replacement = !entry.word.equals(entry.replacement); + show_word_dialog( + getString(R.string.personal_dict_edit_title), + entry.word, + has_replacement ? entry.replacement : "", + new OnSaveListener() + { + @Override + public void on_save(String word, String replacement) + { + _dict.remove(entry.word); + _dict.add(word, replacement); + refresh_list(); + } + }); + } + + interface OnSaveListener + { + void on_save(String word, String replacement); + } + + void show_word_dialog(String title, String initial_word, String initial_replacement, + OnSaveListener listener) + { + LinearLayout form = new LinearLayout(this); + form.setOrientation(LinearLayout.VERTICAL); + int pad = dp(16); + form.setPadding(pad, pad, pad, 0); + + EditText word_field = new EditText(this); + word_field.setHint(R.string.personal_dict_word_hint); + word_field.setText(initial_word); + word_field.setSingleLine(true); + form.addView(word_field); + + EditText replacement_field = new EditText(this); + replacement_field.setHint(R.string.personal_dict_replacement_hint); + replacement_field.setText(initial_replacement); + replacement_field.setSingleLine(true); + form.addView(replacement_field); + + AlertDialog.Builder builder = new AlertDialog.Builder(this) + .setTitle(title) + .setView(form) + .setPositiveButton(R.string.personal_dict_save, new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int which) + { + String word = word_field.getText().toString().trim(); + if (word.isEmpty()) + { + Toast.makeText(PersonalDictionaryActivity.this, + R.string.personal_dict_word_empty, Toast.LENGTH_SHORT).show(); + return; + } + String replacement = replacement_field.getText().toString().trim(); + listener.on_save(word, replacement.isEmpty() ? word : replacement); + } + }) + .setNegativeButton(android.R.string.cancel, null); + + if (!initial_word.isEmpty()) + { + builder.setNeutralButton(R.string.personal_dict_delete, new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int which) + { + _dict.remove(initial_word); + refresh_list(); + } + }); + } + + builder.show(); + } + + void refresh_list() + { + _list_container.removeAllViews(); + List entries = _dict.get_all(); + if (entries.isEmpty()) + { + TextView empty = new TextView(this); + empty.setText(R.string.personal_dict_empty); + int pad = dp(16); + empty.setPadding(pad, pad, pad, pad); + _list_container.addView(empty); + } + else + { + for (PersonalDictionary.Entry e : entries) + { + View row = View.inflate(this, R.layout.personal_dict_entry, null); + ((TextView)row.findViewById(R.id.personal_dict_entry_word)).setText(e.word); + TextView repl_view = (TextView)row.findViewById(R.id.personal_dict_entry_replacement); + boolean has_replacement = !e.word.equals(e.replacement); + repl_view.setText(has_replacement ? ("→ " + e.replacement) : ""); + repl_view.setVisibility(has_replacement ? View.VISIBLE : View.GONE); + row.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View v) { show_edit_dialog(e); } + }); + _list_container.addView(row); + } + } + } + + int dp(int value) + { + return (int)(value * getResources().getDisplayMetrics().density); + } +} diff --git a/srcs/juloo.keyboard2/prefs/DictionaryPickerPreference.java b/srcs/juloo.keyboard2/prefs/DictionaryPickerPreference.java new file mode 100644 index 000000000..20a7ec7fd --- /dev/null +++ b/srcs/juloo.keyboard2/prefs/DictionaryPickerPreference.java @@ -0,0 +1,55 @@ +package juloo.keyboard2.prefs; + +import android.app.AlertDialog; +import android.content.Context; +import android.preference.ListPreference; +import android.util.AttributeSet; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import juloo.keyboard2.R; +import juloo.keyboard2.dict.Dictionaries; +import juloo.keyboard2.dict.SupportedDictionaries; + +/** A [ListPreference] whose entries are built dynamically from the installed + dictionaries. Shows "Auto" plus each installed dictionary by display name. */ +public class DictionaryPickerPreference extends ListPreference +{ + public DictionaryPickerPreference(Context ctx, AttributeSet attrs) + { + super(ctx, attrs); + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) + { + refresh_entries(); + super.onPrepareDialogBuilder(builder); + } + + void refresh_entries() + { + Context ctx = getContext(); + Set installed = Dictionaries.instance(ctx).get_installed(); + SupportedDictionaries supported = new SupportedDictionaries(ctx.getResources()); + + List entries = new ArrayList<>(); + List values = new ArrayList<>(); + + entries.add(ctx.getString(R.string.pref_selected_dictionary_auto)); + values.add("auto"); + + for (int i = 0; i < supported.length(); i++) + { + String name = supported.dict_name(i); + if (installed.contains(name)) + { + entries.add(supported.display_name(i)); + values.add(name); + } + } + + setEntries(entries.toArray(new CharSequence[0])); + setEntryValues(values.toArray(new CharSequence[0])); + } +} diff --git a/srcs/juloo.keyboard2/suggestions/CandidatesView.java b/srcs/juloo.keyboard2/suggestions/CandidatesView.java index f2eb90721..23baa26ec 100644 --- a/srcs/juloo.keyboard2/suggestions/CandidatesView.java +++ b/srcs/juloo.keyboard2/suggestions/CandidatesView.java @@ -1,6 +1,7 @@ package juloo.keyboard2.suggestions; import android.content.Context; +import android.content.Intent; import android.os.Build.VERSION; import android.text.InputType; import android.util.AttributeSet; @@ -15,6 +16,8 @@ import java.util.Locale; import juloo.keyboard2.Config; import juloo.keyboard2.R; +import juloo.keyboard2.dict.PersonalDictionary; +import juloo.keyboard2.dict.PersonalDictionaryActivity; public class CandidatesView extends LinearLayout { @@ -34,6 +37,23 @@ public class CandidatesView extends LinearLayout shown. Might be [null]. */ View _status_no_dict = null; + /** Small button shown while typing to add the current word to the personal + dictionary. Might be [null] until inflated. */ + View _add_word_btn = null; + + /** Word currently being typed, used when the add-word button is tapped. */ + String _current_word = null; + + /** When set, tapping the add-word button will pre-fill the word field with + this value (the original word before autocorrect replaced it). Persists + independently of [_current_word] so it survives the word-tracking reset + that happens when the cursor lands after a space post-undo. */ + String _pending_word = null; + + /** When set, tapping the add-word button will pre-fill the replacement field + with this value (the word that autocorrect had previously inserted). */ + String _pending_replacement = null; + public CandidatesView(Context context, AttributeSet attrs) { super(context, attrs); @@ -47,6 +67,41 @@ protected void onFinishInflate() setup_item_view(1, R.id.candidates_right); setup_item_view(2, R.id.candidates_left); setup_item_view(3, R.id.candidates_emoji); + setup_add_word_button(); + } + + /** Called with the word currently being typed so the add-word button knows + what to pre-fill. Pass [null] when no word is being typed. */ + public void set_current_word(String word) + { + _current_word = word; + } + + /** Called when backspace undoes an autocorrect. Highlights the add-word + button and pre-fills both the original word and the correction. */ + public void on_autocorrect_undone(String original, String corrected) + { + _pending_word = original; + _pending_replacement = corrected; + set_add_word_highlight(true); + } + + void set_add_word_highlight(boolean highlighted) + { + if (_add_word_btn == null) return; + android.util.TypedValue tv = new android.util.TypedValue(); + int colorAttr = highlighted + ? R.attr.colorLabelActivated + : R.attr.colorLabel; + getContext().getTheme().resolveAttribute(colorAttr, tv, true); + ((TextView)_add_word_btn).setTextColor(tv.data); + } + + void clear_add_word_highlight() + { + _pending_word = null; + _pending_replacement = null; + set_add_word_highlight(false); } public void set_candidates(Suggestions s) @@ -87,7 +142,9 @@ public void refresh_config(Config config) clear_candidates(); // The status message indicates whether the dictionaries should be // installed. - if (config.current_dictionary == null) + boolean no_dict = config.current_dictionary == null + && (config.personal_dictionary == null || config.personal_dictionary.get_all().isEmpty()); + if (no_dict) inflate_status_no_dict(config); else if (_status_no_dict != null) _status_no_dict.setVisibility(View.GONE); @@ -143,7 +200,12 @@ void inflate_status_no_dict(Config config) { _status_no_dict = View.inflate(getContext(), R.layout.candidates_status_no_dict, null); - addView(_status_no_dict); + // Insert before the "+" button so it stays at the right edge. + int add_idx = (_add_word_btn != null) ? indexOfChild(_add_word_btn) : -1; + if (add_idx >= 0) + addView(_status_no_dict, add_idx); + else + addView(_status_no_dict); } Locale current_locale = (config.device_locales.default_ != null) ? Locale.forLanguageTag(config.device_locales.default_.lang_tag) : null; @@ -155,6 +217,7 @@ void inflate_status_no_dict(Config config) _status_no_dict.setVisibility(View.VISIBLE); } + private void setup_item_view(final int item_index, int item_id) { TextView v = (TextView)findViewById(item_id); @@ -172,6 +235,31 @@ public void onClick(View _v) _item_views[item_index] = v; } + private void setup_add_word_button() + { + _add_word_btn = findViewById(R.id.candidates_add_word); + if (_add_word_btn == null) + return; + _add_word_btn.setVisibility(View.VISIBLE); + _add_word_btn.setOnClickListener(new View.OnClickListener() + { + @Override + public void onClick(View _v) + { + Intent i = new Intent(getContext(), PersonalDictionaryActivity.class); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + String word = (_pending_word != null && !_pending_word.isEmpty()) + ? _pending_word : _current_word; + if (word != null && !word.isEmpty()) + i.putExtra(PersonalDictionaryActivity.EXTRA_WORD, word); + if (_pending_replacement != null) + i.putExtra(PersonalDictionaryActivity.EXTRA_REPLACEMENT, _pending_replacement); + clear_add_word_highlight(); + getContext().startActivity(i); + } + }); + } + /** Whether the candidates view should be shown for a given editor. */ public static boolean should_show(EditorInfo info) { diff --git a/srcs/juloo.keyboard2/suggestions/Suggestions.java b/srcs/juloo.keyboard2/suggestions/Suggestions.java index f72257b6f..bccf445b0 100644 --- a/srcs/juloo.keyboard2/suggestions/Suggestions.java +++ b/srcs/juloo.keyboard2/suggestions/Suggestions.java @@ -4,6 +4,7 @@ import java.util.List; import juloo.cdict.Cdict; import juloo.keyboard2.dict.Dictionaries; +import juloo.keyboard2.dict.PersonalDictionary; import juloo.keyboard2.Config; import juloo.keyboard2.ComposeKey; import juloo.keyboard2.ComposeKeyData; @@ -39,16 +40,30 @@ public void started() public void currently_typed_word(String word) { + _callback.set_current_word(word.isEmpty() ? null : word); if (!_enabled) return; - if (word.length() < 2 || _config.current_dictionary == null) + Cdict dict = _config.current_dictionary; + PersonalDictionary pdict = _config.personal_dictionary; + boolean has_personal = pdict != null && !pdict.get_all().isEmpty(); + if (word.length() < 2 || (dict == null && !has_personal)) + { clear(); + } else - query_suggestions(word); + { + count = 0; + emoji_suggestion = null; + if (has_personal) + count += pdict.find_suggestions(word, suggestions, 0, MAX_COUNT); + if (dict != null && count < MAX_COUNT) + query_suggestions(word, count); + } set_suggestions(); } void clear() + { count = 0; suggestions[0] = null; @@ -82,11 +97,44 @@ int query_suggestions(String word) return i; } + int query_suggestions(String word, int offset) + { + Cdict dict = _config.current_dictionary; + boolean first_char_upper = Character.isUpperCase(word.charAt(0)); + word = apply_substitutions(word); + Cdict.Result r = dict.find(word); + int i = 0; + int limit = MAX_COUNT - offset; + if (r.found) + suggestions[offset + i++] = dict.word(r.index); + int[] suffixes = dict.suffixes(r, limit); + int[] dist = (word.length() < 3 || i + 1 >= limit) ? NO_RESULTS : + dict.distance(word, 1, limit); + for (int j = 0; j < limit && i < limit; j++) + { + if (suffixes.length > j) + suggestions[offset + i++] = dict.word(suffixes[j]); + if (dist.length > j && i < limit) + suggestions[offset + i++] = dict.word(dist[j]); + } + if (first_char_upper) + capitalize_results(offset, offset + i); + emoji_suggestion = query_emoji(word); + count = offset + i; + return i; + } + void capitalize_results() { - for (int i = 0; i < count; i++) - suggestions[i] = suggestions[i].substring(0, 1).toUpperCase() - + suggestions[i].substring(1); + capitalize_results(0, count); + } + + void capitalize_results(int from, int to) + { + for (int i = from; i < to && i < suggestions.length; i++) + if (suggestions[i] != null) + suggestions[i] = suggestions[i].substring(0, 1).toUpperCase() + + suggestions[i].substring(1); } String query_emoji(String word) @@ -130,5 +178,7 @@ void set_suggestions() public static interface Callback { public void set_suggestions(Suggestions suggestions); + /** Called with the word currently being typed, or [null] when no word. */ + public void set_current_word(String word); } } diff --git a/srcs/layouts/latn_dvorak.xml b/srcs/layouts/latn_dvorak.xml index 656320097..ebc960481 100644 --- a/srcs/layouts/latn_dvorak.xml +++ b/srcs/layouts/latn_dvorak.xml @@ -1,37 +1,53 @@ - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + + - + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/srcs/layouts/latn_dvorak_landscape.xml b/srcs/layouts/latn_dvorak_landscape.xml new file mode 100644 index 000000000..1962ba980 --- /dev/null +++ b/srcs/layouts/latn_dvorak_landscape.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/srcs/layouts/latn_dvorak_left.xml b/srcs/layouts/latn_dvorak_left.xml new file mode 100644 index 000000000..62ef8a75b --- /dev/null +++ b/srcs/layouts/latn_dvorak_left.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/srcs/special_font/01D.svg b/srcs/special_font/01D.svg new file mode 100644 index 000000000..275c2a396 --- /dev/null +++ b/srcs/special_font/01D.svg @@ -0,0 +1,50 @@ + + + + + + + + + + +