From 601b99494103b5d9338aeaf286cdf2b69e10eb85 Mon Sep 17 00:00:00 2001 From: Helium314 Date: Wed, 28 Jun 2023 20:40:35 +0200 Subject: [PATCH] allow removing suggestions --- README.md | 50 ++++---- .../inputmethod/keyboard/KeyboardView.java | 10 -- .../latin/DictionaryFacilitator.java | 2 + .../latin/DictionaryFacilitatorImpl.java | 120 +++++++++++++++++- .../openboard/inputmethod/latin/LatinIME.java | 5 + .../suggestions/SuggestionStripView.java | 56 +++++++- app/src/main/res/values/strings.xml | 2 + 7 files changed, 209 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 909130c6e..cac0c2ab0 100644 --- a/README.md +++ b/README.md @@ -5,30 +5,6 @@ Might end up on F-Droid... **consider all releases as beta** -Plan / to do: -* ~upgrade dependencies~ -* upgrade NDK, https://github.com/openboard-team/openboard/issues/782 -* maybe: rename (package, app, icon), so it can be installed parallel to OpenBoard, and published on F-Droid -* ~user-selectable dictionaries, https://github.com/openboard-team/openboard/pull/578~ - * ~make additional dictionaries available for download (from OpenBoard PRs)~ - * more dictionaries - * proper icon for preference -* ~multi-lingual typing, https://github.com/openboard-team/openboard/pull/586, https://github.com/openboard-team/openboard/pull/593~ - * maybe improve way of merging suggestions from both languages - * test whether it works reasonably well in non-latin scripts -* ~suggestion fixes, https://github.com/openboard-team/openboard/pull/694, https://github.com/openboard-team/openboard/issues/795, https://github.com/openboard-team/openboard/issues/660~ -* ~improve auto-space insertion, https://github.com/openboard-team/openboard/pull/576~ -* emoji prediction/search, either https://github.com/openboard-team/openboard/pull/749 (using emoji dictionaries already possible) -* ~theming, https://github.com/openboard-team/openboard/issues/124~ - * fix emoji view not themed properly - * fix ABC buttons in emoji and clipboard view have wrong text color - * fix buttons on long-press action key not themed - * allow adjusting colors without requiring manual reload of keyboard -* delete suggestions, https://github.com/openboard-team/openboard/issues/106 -* ~gesture typing, https://github.com/openboard-team/openboard/issues/3~ - * ~license issues, require using an external library~ -* re-consider preferring lowercase word for typed history in some cases (DictionaryFacilitatorImpl.addWordToUserHistory) - Changes: * Updated dependencies * Debug version can be installed along OpenBoard @@ -48,6 +24,32 @@ Changes: * based on wordmage's work https://github.com/openboard-team/openboard/tree/57d33791d7674e3fe0600eddb72f6b4317b5df00 * tested with Google libraries and [others](https://github.com/openboard-team/openboard/issues/3#issuecomment-1200456262) (when building with the [rename](https://github.com/openboard-team/openboard/tree/57d33791d7674e3fe0600eddb72f6b4317b5df00)) * Allow adjusting keyboard colors, https://github.com/openboard-team/openboard/issues/124 +* Remove suggestions by long pressing on suggestion strip while the more suggestions popup is open, https://github.com/openboard-team/openboard/issues/106 + * suggestions get re-added if they are entered again + +Plan / to do: +* ~upgrade dependencies~ +* upgrade NDK, https://github.com/openboard-team/openboard/issues/782 +* maybe: rename (package, app, icon), so it can be installed parallel to OpenBoard, and published on F-Droid +* ~user-selectable dictionaries, https://github.com/openboard-team/openboard/pull/578~ + * ~make additional dictionaries available for download (from OpenBoard PRs)~ + * more dictionaries + * proper icon for preference +* ~multi-lingual typing, https://github.com/openboard-team/openboard/pull/586, https://github.com/openboard-team/openboard/pull/593~ + * maybe improve way of merging suggestions from both languages + * test whether it works reasonably well in non-latin scripts +* ~suggestion fixes, https://github.com/openboard-team/openboard/pull/694, https://github.com/openboard-team/openboard/issues/795, https://github.com/openboard-team/openboard/issues/660~ +* ~improve auto-space insertion, https://github.com/openboard-team/openboard/pull/576~ +* emoji prediction/search, either https://github.com/openboard-team/openboard/pull/749 (using emoji dictionaries already possible) +* ~theming, https://github.com/openboard-team/openboard/issues/124~ + * fix emoji view not themed properly + * fix ABC buttons in emoji and clipboard view have wrong text color + * fix buttons on long-press action key not themed + * allow adjusting colors without requiring manual reload of keyboard +* ~delete suggestions, https://github.com/openboard-team/openboard/issues/106~ +* ~gesture typing, https://github.com/openboard-team/openboard/issues/3~ + * ~license issues, require using an external library~ +* re-consider preferring lowercase word for typed history in some cases (DictionaryFacilitatorImpl.addWordToUserHistory) ----- diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardView.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardView.java index a6d658cff..4052ae32c 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardView.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardView.java @@ -396,16 +396,6 @@ public class KeyboardView extends View { bgX = -padding.left; bgY = -padding.top; } -/* if (mUserTheme) { - // color filter is applied to background, which is re-used - // but we don't want it applied to "blue" keys - // so we always need to select the color filter dependent on the current key - if (key.isActionKey() - || (key.getBackgroundType() == Key.BACKGROUND_TYPE_NORMAL && key.getCode() < 0 && key.getCode() != Constants.CODE_SWITCH_ALPHA_SYMBOL)) - background.clearColorFilter(); - else - background.setColorFilter(keyBgFilter); - }*/ if (mUserTheme) { // color filter is applied to background, which is re-used // but we don't want it applied to "blue" keys diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/DictionaryFacilitator.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/DictionaryFacilitator.java index 4d78bb1a3..a067cdabe 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/DictionaryFacilitator.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/DictionaryFacilitator.java @@ -121,6 +121,8 @@ public interface DictionaryFacilitator { final String dictNamePrefix, @Nullable final DictionaryInitializationListener listener); + void removeWord(String word); + @UsedForTesting void resetDictionariesForTesting( final Context context, diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/DictionaryFacilitatorImpl.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/DictionaryFacilitatorImpl.java index 06a880fba..4d06d7aaf 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/DictionaryFacilitatorImpl.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/DictionaryFacilitatorImpl.java @@ -38,8 +38,11 @@ import org.dslul.openboard.inputmethod.latin.utils.ScriptUtils; import org.dslul.openboard.inputmethod.latin.utils.SuggestionResults; import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -47,6 +50,8 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Scanner; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -155,6 +160,10 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { // in this language. private int mConfidence = 1; + // words cannot be removed from main dictionary, so we use a blacklist instead + public String blacklistFileName = null; + public Set blacklist = new HashSet<>(); + // allow to go above max confidence, for better determination of currently preferred language // when decreasing confidence or getting weight factor, limit to maximum public void increaseConfidence() { @@ -455,6 +464,20 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { mSecondaryDictionaryGroup == null ? null : secondaryLocale, listener); } } + + // load blacklists + mDictionaryGroup.blacklistFileName = context.getFilesDir().getAbsolutePath() + File.separator + "blacklists" + File.separator + newLocale.toString().toLowerCase(Locale.ENGLISH) + ".txt"; + if (!new File(mDictionaryGroup.blacklistFileName).exists()) + new File(context.getFilesDir().getAbsolutePath() + File.separator + "blacklists").mkdirs(); + mDictionaryGroup.blacklist.addAll(readBlacklistFile(mDictionaryGroup.blacklistFileName)); + + if (mSecondaryDictionaryGroup != null) { + mSecondaryDictionaryGroup.blacklistFileName = context.getFilesDir().getAbsolutePath() + File.separator + "blacklists" + File.separator + secondaryLocale.toString().toLowerCase(Locale.ENGLISH) + ".txt"; + if (!new File(mSecondaryDictionaryGroup.blacklistFileName).exists()) + new File(context.getFilesDir().getAbsolutePath() + File.separator + "blacklists").mkdirs(); + mSecondaryDictionaryGroup.blacklist.addAll(readBlacklistFile(mSecondaryDictionaryGroup.blacklistFileName)); + } + if (listener != null) { listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary()); } @@ -655,6 +678,12 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { blockPotentiallyOffensive); ngramContextForCurrentWord = ngramContextForCurrentWord.getNextNgramContext(new WordInfo(currentWord)); + + // remove entered words from blacklist + if (mDictionaryGroup.blacklist.remove(currentWord)) + removeWordFromBlacklistFile(currentWord, mDictionaryGroup.blacklistFileName); + if (mSecondaryDictionaryGroup != null && mSecondaryDictionaryGroup.blacklist.remove(currentWord)) + removeWordFromBlacklistFile(currentWord, mSecondaryDictionaryGroup.blacklistFileName); } } @@ -834,7 +863,14 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { proximityInfoHandle, settingsValuesForSuggestion, sessionId, weightForLocale, weightOfLangModelVsSpatialModel); if (null == dictionarySuggestions) continue; - suggestions.addAll(dictionarySuggestions); + + // don't add blacklisted words + // this may not be the most efficient way, but getting suggestions is much slower anyway + for (SuggestedWordInfo info : dictionarySuggestions) { + if (!isBlacklisted(info.getWord())) { + suggestions.add(info); + } + } } return suggestions; } @@ -865,6 +901,7 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { if (dictionaryGroup.mLocale == null) { return false; } + if (isBlacklisted(word)) return false; for (final String dictType : dictionariesToCheck) { final Dictionary dictionary = dictionaryGroup.getDict(dictType); // Ideally the passed map would come out of a {@link java.util.concurrent.Future} and @@ -878,6 +915,87 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { return false; } + private boolean isBlacklisted(final String word) { + if (mDictionaryGroup.blacklist.contains(word)) + return true; + if (mSecondaryDictionaryGroup != null && mSecondaryDictionaryGroup.blacklist.contains(word)) + return true; + return false; + } + + @Override + public void removeWord(String word) { + removeWordFromGroup(word, mDictionaryGroup); + if (mSecondaryDictionaryGroup != null) + removeWordFromGroup(word, mSecondaryDictionaryGroup); + } + + private void removeWordFromGroup(String word, DictionaryGroup group) { + // remove from user history + final ExpandableBinaryDictionary historyDict = group.getSubDict(Dictionary.TYPE_USER_HISTORY); + if (historyDict != null) { + historyDict.removeUnigramEntryDynamically(word); + } + // and from personal dictionary + final ExpandableBinaryDictionary userDict = group.getSubDict(Dictionary.TYPE_USER); + if (userDict != null) { + userDict.removeUnigramEntryDynamically(word); + } + + // add to blacklist if in main dictionary + if (group.getDict(Dictionary.TYPE_MAIN).isValidWord(word) && group.blacklist.add(word)) { + // write to file if word wasn't already in blacklist + ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(new Runnable() { + @Override + public void run() { + try { + FileOutputStream fos = new FileOutputStream(group.blacklistFileName, true); + fos.write((word + "\n").getBytes(StandardCharsets.UTF_8)); + fos.close(); + } catch (IOException e) { + Log.e(TAG, "Exception while trying to write blacklist", e); + } + } + }); + } + } + + private ArrayList readBlacklistFile(final String filename) { + final ArrayList blacklist = new ArrayList<>(); + if (filename == null) return blacklist; + File blacklistFile = new File(filename); + if (!blacklistFile.exists()) return blacklist; + try { + final Scanner scanner = new Scanner(blacklistFile, StandardCharsets.UTF_8.name()).useDelimiter("\n"); + while (scanner.hasNext()) { + blacklist.add(scanner.next()); + } + } catch (IOException e) { + Log.e(TAG, "Exception while reading blacklist", e); + } + return blacklist; + } + + private void removeWordFromBlacklistFile(String word, String filename) { + ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(new Runnable() { + @Override + public void run() { + try { + ArrayList blacklist = readBlacklistFile(filename); + blacklist.remove(word); + FileOutputStream fos = new FileOutputStream(filename); + for (String entry : blacklist) { + fos.write((entry + "\n").getBytes(StandardCharsets.UTF_8)); + } + fos.close(); + } catch (IOException e) { + Log.e(TAG, "Exception while trying to write blacklist" + filename, e); + } + } + }); + + } + // called from addWordToUserHistory with a specified dictionary, so provide this dictionary private int getFrequency(final String word, DictionaryGroup dictGroup) { if (TextUtils.isEmpty(word)) { diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/LatinIME.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/LatinIME.java index 12ea21299..47ec47300 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/LatinIME.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/LatinIME.java @@ -1684,6 +1684,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen setSuggestedWords(neutralSuggestions); } + @Override + public void removeSuggestion(final String word) { + mDictionaryFacilitator.removeWord(word); + } + // Outside LatinIME, only used by the {@link InputTestsBase} test suite. @UsedForTesting void loadKeyboard() { diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/latin/suggestions/SuggestionStripView.java b/app/src/main/java/org/dslul/openboard/inputmethod/latin/suggestions/SuggestionStripView.java index 641d57c67..53893f611 100644 --- a/app/src/main/java/org/dslul/openboard/inputmethod/latin/suggestions/SuggestionStripView.java +++ b/app/src/main/java/org/dslul/openboard/inputmethod/latin/suggestions/SuggestionStripView.java @@ -27,6 +27,7 @@ import android.util.AttributeSet; import android.util.TypedValue; import android.view.GestureDetector; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; @@ -35,6 +36,7 @@ import android.view.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import android.widget.ImageButton; +import android.widget.PopupMenu; import android.widget.RelativeLayout; import android.widget.TextView; @@ -51,6 +53,7 @@ import org.dslul.openboard.inputmethod.latin.define.DebugFlags; import org.dslul.openboard.inputmethod.latin.settings.Settings; import org.dslul.openboard.inputmethod.latin.settings.SettingsValues; import org.dslul.openboard.inputmethod.latin.suggestions.MoreSuggestionsView.MoreSuggestionsListener; +import org.dslul.openboard.inputmethod.latin.utils.DialogUtils; import java.util.ArrayList; @@ -62,6 +65,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick void pickSuggestionManually(SuggestedWordInfo word); void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat); void onTextInput(final String rawText); + void removeSuggestion(final String word); CharSequence getSelection(); } @@ -290,7 +294,57 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick } AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback( Constants.NOT_A_CODE, this); - return showMoreSuggestions(); + if (isShowingMoreSuggestionPanel() || !showMoreSuggestions()) { + for (int i = 0; i < mStartIndexOfMoreSuggestions; i++) { + if (view == mWordViews.get(i)) { + showDeleteSuggestionDialog(mWordViews.get(i)); + return true; + } + } + } + return true; + } + + private void showDeleteSuggestionDialog(final TextView wordView) { + final String word = wordView.getText().toString(); + + final PopupMenu menu = new PopupMenu(DialogUtils.getPlatformDialogThemeContext(getContext()), wordView); + menu.getMenu().add(R.string.remove_suggestions); + menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + mListener.removeSuggestion(word); + mMoreSuggestionsView.dismissMoreKeysPanel(); + // show suggestions, but without the removed word + final ArrayList sw = new ArrayList(); + for (int i = 0; i < mSuggestedWords.size(); i ++) { + final SuggestedWordInfo info = mSuggestedWords.getInfo(i); + if (!info.getWord().equals(word)) + sw.add(info); + } + ArrayList rs = null; + if (mSuggestedWords.mRawSuggestions != null) { + rs = mSuggestedWords.mRawSuggestions; + for (int i = 0; i < rs.size(); i ++) { + if (rs.get(i).getWord().equals(word)) { + rs.remove(i); + break; + } + } + } + // copied code from setSuggestions, but without the Rtl part + clear(); + mSuggestedWords = new SuggestedWords(sw, rs, mSuggestedWords.getTypedWordInfo(), + mSuggestedWords.mTypedWordValid, mSuggestedWords.mWillAutoCorrect, + mSuggestedWords.mIsObsoleteSuggestions, mSuggestedWords.mInputStyle, + mSuggestedWords.mSequenceNumber); + mStartIndexOfMoreSuggestions = mLayoutHelper.layoutAndReturnStartIndexOfMoreSuggestions( + getContext(), mSuggestedWords, mSuggestionsStrip, SuggestionStripView.this); + mStripVisibilityGroup.showSuggestionsStrip(); + return true; + } + }); + menu.show(); } boolean showMoreSuggestions() { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0b19a7a9e..00b982c36 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -154,6 +154,8 @@ Next-word suggestions Use the previous word in making suggestions + + Remove suggestion Enable gesture typing