allow removing suggestions

This commit is contained in:
Helium314 2023-06-28 20:40:35 +02:00
parent cdabf650c6
commit 601b994941
7 changed files with 209 additions and 36 deletions

View file

@ -5,30 +5,6 @@ Might end up on F-Droid...
**consider all releases as beta** **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: Changes:
* Updated dependencies * Updated dependencies
* Debug version can be installed along OpenBoard * 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 * 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)) * 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 * 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)
----- -----

View file

@ -396,16 +396,6 @@ public class KeyboardView extends View {
bgX = -padding.left; bgX = -padding.left;
bgY = -padding.top; 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) { if (mUserTheme) {
// color filter is applied to background, which is re-used // color filter is applied to background, which is re-used
// but we don't want it applied to "blue" keys // but we don't want it applied to "blue" keys

View file

@ -121,6 +121,8 @@ public interface DictionaryFacilitator {
final String dictNamePrefix, final String dictNamePrefix,
@Nullable final DictionaryInitializationListener listener); @Nullable final DictionaryInitializationListener listener);
void removeWord(String word);
@UsedForTesting @UsedForTesting
void resetDictionariesForTesting( void resetDictionariesForTesting(
final Context context, final Context context,

View file

@ -38,8 +38,11 @@ import org.dslul.openboard.inputmethod.latin.utils.ScriptUtils;
import org.dslul.openboard.inputmethod.latin.utils.SuggestionResults; import org.dslul.openboard.inputmethod.latin.utils.SuggestionResults;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -47,6 +50,8 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -155,6 +160,10 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
// in this language. // in this language.
private int mConfidence = 1; private int mConfidence = 1;
// words cannot be removed from main dictionary, so we use a blacklist instead
public String blacklistFileName = null;
public Set<String> blacklist = new HashSet<>();
// allow to go above max confidence, for better determination of currently preferred language // allow to go above max confidence, for better determination of currently preferred language
// when decreasing confidence or getting weight factor, limit to maximum // when decreasing confidence or getting weight factor, limit to maximum
public void increaseConfidence() { public void increaseConfidence() {
@ -455,6 +464,20 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
mSecondaryDictionaryGroup == null ? null : secondaryLocale, listener); 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) { if (listener != null) {
listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary()); listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary());
} }
@ -655,6 +678,12 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
blockPotentiallyOffensive); blockPotentiallyOffensive);
ngramContextForCurrentWord = ngramContextForCurrentWord =
ngramContextForCurrentWord.getNextNgramContext(new WordInfo(currentWord)); 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, proximityInfoHandle, settingsValuesForSuggestion, sessionId,
weightForLocale, weightOfLangModelVsSpatialModel); weightForLocale, weightOfLangModelVsSpatialModel);
if (null == dictionarySuggestions) continue; 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; return suggestions;
} }
@ -865,6 +901,7 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
if (dictionaryGroup.mLocale == null) { if (dictionaryGroup.mLocale == null) {
return false; return false;
} }
if (isBlacklisted(word)) return false;
for (final String dictType : dictionariesToCheck) { for (final String dictType : dictionariesToCheck) {
final Dictionary dictionary = dictionaryGroup.getDict(dictType); final Dictionary dictionary = dictionaryGroup.getDict(dictType);
// Ideally the passed map would come out of a {@link java.util.concurrent.Future} and // 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; 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<String> readBlacklistFile(final String filename) {
final ArrayList<String> 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<String> 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 // called from addWordToUserHistory with a specified dictionary, so provide this dictionary
private int getFrequency(final String word, DictionaryGroup dictGroup) { private int getFrequency(final String word, DictionaryGroup dictGroup) {
if (TextUtils.isEmpty(word)) { if (TextUtils.isEmpty(word)) {

View file

@ -1684,6 +1684,11 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen
setSuggestedWords(neutralSuggestions); setSuggestedWords(neutralSuggestions);
} }
@Override
public void removeSuggestion(final String word) {
mDictionaryFacilitator.removeWord(word);
}
// Outside LatinIME, only used by the {@link InputTestsBase} test suite. // Outside LatinIME, only used by the {@link InputTestsBase} test suite.
@UsedForTesting @UsedForTesting
void loadKeyboard() { void loadKeyboard() {

View file

@ -27,6 +27,7 @@ import android.util.AttributeSet;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.GestureDetector; import android.view.GestureDetector;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
@ -35,6 +36,7 @@ import android.view.ViewGroup;
import android.view.ViewParent; import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityEvent;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.PopupMenu;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; 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.Settings;
import org.dslul.openboard.inputmethod.latin.settings.SettingsValues; import org.dslul.openboard.inputmethod.latin.settings.SettingsValues;
import org.dslul.openboard.inputmethod.latin.suggestions.MoreSuggestionsView.MoreSuggestionsListener; import org.dslul.openboard.inputmethod.latin.suggestions.MoreSuggestionsView.MoreSuggestionsListener;
import org.dslul.openboard.inputmethod.latin.utils.DialogUtils;
import java.util.ArrayList; import java.util.ArrayList;
@ -62,6 +65,7 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
void pickSuggestionManually(SuggestedWordInfo word); void pickSuggestionManually(SuggestedWordInfo word);
void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat); void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat);
void onTextInput(final String rawText); void onTextInput(final String rawText);
void removeSuggestion(final String word);
CharSequence getSelection(); CharSequence getSelection();
} }
@ -290,7 +294,57 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick
} }
AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback( AudioAndHapticFeedbackManager.getInstance().performHapticAndAudioFeedback(
Constants.NOT_A_CODE, this); 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<SuggestedWordInfo> sw = new ArrayList<SuggestedWordInfo>();
for (int i = 0; i < mSuggestedWords.size(); i ++) {
final SuggestedWordInfo info = mSuggestedWords.getInfo(i);
if (!info.getWord().equals(word))
sw.add(info);
}
ArrayList<SuggestedWordInfo> 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() { boolean showMoreSuggestions() {

View file

@ -154,6 +154,8 @@
<string name="bigram_prediction">Next-word suggestions</string> <string name="bigram_prediction">Next-word suggestions</string>
<!-- Description for "next word suggestion" option. This displays suggestions even when there is no input, based on the previous word. --> <!-- Description for "next word suggestion" option. This displays suggestions even when there is no input, based on the previous word. -->
<string name="bigram_prediction_summary">Use the previous word in making suggestions</string> <string name="bigram_prediction_summary">Use the previous word in making suggestions</string>
<!-- Menu item for removing a suggestion-->
<string name="remove_suggestions">Remove suggestion</string>
<!-- Option to enable gesture input. The user can input a word by tracing the letters of a word without releasing the finger from the screen. [CHAR LIMIT=30]--> <!-- Option to enable gesture input. The user can input a word by tracing the letters of a word without releasing the finger from the screen. [CHAR LIMIT=30]-->
<string name="gesture_input">Enable gesture typing</string> <string name="gesture_input">Enable gesture typing</string>
<!-- Description for "gesture_input" option. The user can input a word by tracing the letters of a word without releasing the finger from the screen. [CHAR LIMIT=65]--> <!-- Description for "gesture_input" option. The user can input a word by tracing the letters of a word without releasing the finger from the screen. [CHAR LIMIT=65]-->