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 @@
+
+
+
+