From 5f1ee64952e45f18f9d8697976b9369d933af82a Mon Sep 17 00:00:00 2001 From: Emil Kosz Date: Sun, 3 Nov 2024 18:44:31 +0100 Subject: [PATCH] Clipboard managment pretty much finished --- ...ClipboardHandler.java => ClipManager.java} | 44 ++++++---- .../cfpi_fpsi/eu/titanboard/InputMethod.java | 7 +- .../cfpi_fpsi/eu/titanboard/InputView.java | 83 ++++++++++++++----- app/src/main/res/drawable/button.xml | 18 ++-- .../main/res/drawable/button_transparent.xml | 31 +++++++ app/src/main/res/drawable/candidate.xml | 10 ++- app/src/main/res/drawable/clipboard.xml | 71 ++++++++++++++++ .../candidate.xml => drawable/popup.xml} | 15 ++-- app/src/main/res/drawable/trashcan.xml | 61 ++++++++++++++ app/src/main/res/layout/clip.xml | 52 ++++++++++++ app/src/main/res/layout/clipboard.xml | 31 +++++++ app/src/main/res/layout/input.xml | 46 +++++----- app/src/main/res/layout/symbol.xml | 5 +- .../main/res/values/{styles.xml => attrs.xml} | 15 +--- app/src/main/res/values/colors.xml | 29 +++++++ app/src/main/res/values/dimens.xml | 12 +-- app/src/main/res/values/strings.xml | 12 ++- app/src/main/res/xml/preferences.xml | 1 + 18 files changed, 435 insertions(+), 108 deletions(-) rename app/src/main/java/cfpi_fpsi/eu/titanboard/{ClipboardHandler.java => ClipManager.java} (56%) create mode 100644 app/src/main/res/drawable/button_transparent.xml create mode 100644 app/src/main/res/drawable/clipboard.xml rename app/src/main/res/{layout/candidate.xml => drawable/popup.xml} (67%) create mode 100644 app/src/main/res/drawable/trashcan.xml create mode 100644 app/src/main/res/layout/clip.xml create mode 100644 app/src/main/res/layout/clipboard.xml rename app/src/main/res/values/{styles.xml => attrs.xml} (65%) create mode 100644 app/src/main/res/values/colors.xml diff --git a/app/src/main/java/cfpi_fpsi/eu/titanboard/ClipboardHandler.java b/app/src/main/java/cfpi_fpsi/eu/titanboard/ClipManager.java similarity index 56% rename from app/src/main/java/cfpi_fpsi/eu/titanboard/ClipboardHandler.java rename to app/src/main/java/cfpi_fpsi/eu/titanboard/ClipManager.java index 104ccd1..0d62932 100644 --- a/app/src/main/java/cfpi_fpsi/eu/titanboard/ClipboardHandler.java +++ b/app/src/main/java/cfpi_fpsi/eu/titanboard/ClipManager.java @@ -25,15 +25,16 @@ import android.content.ClipboardManager; import android.content.Context; import android.os.Build; -import java.util.ArrayDeque; +import java.util.ArrayList; @TargetApi(Build.VERSION_CODES.HONEYCOMB) -public class ClipboardHandler implements ClipboardManager.OnPrimaryClipChangedListener { +public class ClipManager extends ArrayList + implements ClipboardManager.OnPrimaryClipChangedListener { private final InputMethod mInputMethod; private final ClipboardManager mClipboardManager; - private ArrayDeque mClips = new ArrayDeque<>(); + private long mLastCallbackTimestamp; - public ClipboardHandler(Context context) { + public ClipManager(Context context) { mInputMethod = (InputMethod) context; mClipboardManager = (ClipboardManager) mInputMethod.getSystemService(Context.CLIPBOARD_SERVICE); mClipboardManager.addPrimaryClipChangedListener(this); @@ -41,20 +42,31 @@ public class ClipboardHandler implements ClipboardManager.OnPrimaryClipChangedLi @Override public void onPrimaryClipChanged() { - // The callback can be launched several times, so we have to check if it isn't the same - // clipboard data. - if (mClips.peekFirst() != null) { - if (mClips.peekFirst().equals(mClipboardManager.getPrimaryClip())) { - return; + // The callback can be launched several times at once, so we have to check if it isn't the + // same clipboard data. + if (System.currentTimeMillis() - mLastCallbackTimestamp < 100) { + return; + } + + mLastCallbackTimestamp = System.currentTimeMillis(); + ClipData clip = mClipboardManager.getPrimaryClip(); + + if (clip == null) { + return; + } + + for (int i = 0; i < clip.getItemCount(); ++i) { + ClipData.Item item = clip.getItemAt(i); + // We do not support non-text clips for now, so all we add are text clips. + if (item.getText() != null) { + add(0, item); + } + + if (size() > mInputMethod.mPreferenceHolder.getMaxClipboardItems()) { + remove(size() - 1); } } - mClips.addFirst(mClipboardManager.getPrimaryClip()); - - if (mClips.size() > mInputMethod.mPreferenceHolder.getMaxClipboardItems()) { - mClips.removeLast(); - } - - mInputMethod.mInputView.setClips(mClips.toArray()); + mInputMethod.mInputView.setClips(this); } } diff --git a/app/src/main/java/cfpi_fpsi/eu/titanboard/InputMethod.java b/app/src/main/java/cfpi_fpsi/eu/titanboard/InputMethod.java index cc9c75b..dde5c80 100644 --- a/app/src/main/java/cfpi_fpsi/eu/titanboard/InputMethod.java +++ b/app/src/main/java/cfpi_fpsi/eu/titanboard/InputMethod.java @@ -45,12 +45,12 @@ public class InputMethod extends InputMethodService { public MetaKeyManager mMetaKeyManager; public KeymapManager mKeymapManager; public PreferenceHolder mPreferenceHolder; - private ClipboardHandler mClipboardHandler; + public ClipManager mClipManager; public InputView mInputView; private int mCurrentCursorPosition; private Handler mHandler; private Cycler mSymbolCycler; - private Vibrator mVibrator; + public Vibrator mVibrator; @Override public void onCreate() { @@ -59,7 +59,7 @@ public class InputMethod extends InputMethodService { mVibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE); mMetaKeyManager = new MetaKeyManager(mVibrator); mKeymapManager = new KeymapManager(this); - mClipboardHandler = new ClipboardHandler(this); + mClipManager = new ClipManager(this); mHandler = new Handler(Looper.getMainLooper()); mSymbolCycler = new SymbolCycler(); mKeymapManager.cycleKeymaps(mPreferenceHolder.getSelectedKeymaps()); @@ -166,6 +166,7 @@ public class InputMethod extends InputMethodService { @Override public void onFinishInput() { mMetaKeyManager.resetMetaKeyState(); + mInputView.mClipboardPopup.dismiss(); } // Cyclers diff --git a/app/src/main/java/cfpi_fpsi/eu/titanboard/InputView.java b/app/src/main/java/cfpi_fpsi/eu/titanboard/InputView.java index 01e106e..0a68fc2 100644 --- a/app/src/main/java/cfpi_fpsi/eu/titanboard/InputView.java +++ b/app/src/main/java/cfpi_fpsi/eu/titanboard/InputView.java @@ -29,23 +29,30 @@ import android.os.Build; import android.util.AttributeSet; import android.widget.Button; import android.widget.HorizontalScrollView; +import android.widget.ImageButton; import android.widget.LinearLayout; +import android.widget.PopupWindow; import android.widget.RelativeLayout; +import android.widget.ScrollView; import android.widget.TextView; -import java.util.ArrayDeque; import java.util.ArrayList; // TODO: programmatically scroll if current selected symbol is outside the visible space of the // scrollview public class InputView extends RelativeLayout { + protected final int[] BUTTON_STATE_TOGGLED = { R.attr.state_toggled }; + private final InputMethod mInputMethod; private final LinearLayout.LayoutParams mSymbolLayout; - private final LinearLayout.LayoutParams mCandidateLayout; + private final LinearLayout.LayoutParams mClipLayout; private LinearLayout mSymbolList; - private LinearLayout mClipsList; + private LinearLayout mClipList; private Button mStateButton; + private ImageButton mClipboardButton; + private ScrollView mClipboardView; + public PopupWindow mClipboardPopup; public InputView(Context context, AttributeSet attrs) { super(context, attrs, 0); @@ -53,18 +60,17 @@ public class InputView extends RelativeLayout { mSymbolLayout = new LinearLayout.LayoutParams( getResources().getDimensionPixelSize(R.dimen.symbol_width), LinearLayout.LayoutParams.MATCH_PARENT, 0); - mCandidateLayout = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.MATCH_PARENT, 0); + mClipLayout = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT, 0); mSymbolLayout.setMargins(0, 0, getResources().getDimensionPixelSize(R.dimen.default_space), 0); - mCandidateLayout.setMargins(0, 0, - getResources().getDimensionPixelSize(R.dimen.default_space), 0); + mClipLayout.setMargins(0, + getResources().getDimensionPixelSize(R.dimen.default_space), 0, 0); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { mSymbolLayout.setMarginEnd(getResources().getDimensionPixelSize(R.dimen.default_space)); - mCandidateLayout.setMarginEnd(getResources().getDimensionPixelSize(R.dimen.default_space)); } } @@ -72,8 +78,14 @@ public class InputView extends RelativeLayout { protected void onFinishInflate() { super.onFinishInflate(); mSymbolList = findViewById(R.id.symbol_list); - mClipsList = findViewById(R.id.clip_list); mStateButton = findViewById(R.id.state_button); + mClipboardButton = findViewById(R.id.clipboard_button); + mClipboardView = (ScrollView) inflate(this.getContext(), R.layout.clipboard, null); + mClipList = mClipboardView.findViewById(R.id.clip_list); + mClipboardPopup = new PopupWindow(mClipboardView, + getResources().getDimensionPixelSize(R.dimen.clipboard_popup_width), + getResources().getDimensionPixelSize(R.dimen.clipboard_popup_height), true); + mClipboardPopup.setOnDismissListener(() -> mClipboardButton.setImageState(null, true)); mStateButton.setOnClickListener(button -> { MetaKeyManager metaManager = mInputMethod.mMetaKeyManager; @@ -89,6 +101,24 @@ public class InputView extends RelativeLayout { Intent intent = new Intent(getContext(), PreferenceActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); getContext().startActivity(intent); + // For some reason, this vibrates by default, so we don't need to call the vibrator. + return true; + }); + + mClipboardButton.setOnClickListener(button -> { + mClipboardPopup.showAsDropDown(mClipboardButton, + -(mClipboardPopup.getWidth() - mClipboardButton.getWidth()) / 2, 0); + mClipboardButton.setImageState(BUTTON_STATE_TOGGLED, true); + }); + + mClipboardButton.setOnLongClickListener(button -> { + // On a long click, we just send the latest clip (if any). + if (!mInputMethod.mClipManager.isEmpty() + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mInputMethod.getCurrentInputConnection() + .commitText(mInputMethod.mClipManager.get(0).getText(), 1); + } + return true; }); } @@ -135,26 +165,33 @@ public class InputView extends RelativeLayout { } @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public void setClips(Object[] clips) { - for (int i = 0; i < clips.length; ++i) { - Button b = (Button) mClipsList.getChildAt(i); - ClipData clip = (ClipData) clips[i]; + public void setClips(ArrayList items) { + for (int i = 0; i < items.size(); ++i) { + ClipData.Item item = items.get(i); + LinearLayout clipView = (LinearLayout) mClipList.getChildAt(i); - if (b == null) { - b = (Button) inflate(this.getContext(), R.layout.candidate, null); - b.setTag(i); - b.setLayoutParams(mCandidateLayout); - mClipsList.addView(b); + if (clipView == null) { + clipView = (LinearLayout) inflate(this.getContext(), R.layout.clip, null); + clipView.setTag(i); + clipView.setLayoutParams(mClipLayout); + mClipList.addView(clipView); } - ClipData.Item item = clip.getItemAt(0); - b.setText(item.coerceToText(this.getContext()).toString()); + Button pasteButton = clipView.findViewById(R.id.paste_button); + pasteButton.setText(item.getText()); + + pasteButton.setOnClickListener(button -> mInputMethod.getCurrentInputConnection().commitText(item.getText(), 1)); + + LinearLayout finalClipView = clipView; + clipView.findViewById(R.id.button_delete).setOnClickListener(button -> { + mClipList.removeView(finalClipView); + mInputMethod.mClipManager.remove(item); + }); } } public void setSymbolsVisible(boolean state) { - findViewById(R.id.symbol_list_container).setVisibility(state ? VISIBLE : GONE); - findViewById(R.id.clip_list_container).setVisibility(state ? GONE : VISIBLE); + mSymbolList.setVisibility(state ? VISIBLE : GONE); } public void setSymbols(ArrayList strings) { diff --git a/app/src/main/res/drawable/button.xml b/app/src/main/res/drawable/button.xml index 7c436f2..f3601be 100644 --- a/app/src/main/res/drawable/button.xml +++ b/app/src/main/res/drawable/button.xml @@ -17,17 +17,25 @@ ~ --> - + + + + + + + - - + + - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/button_transparent.xml b/app/src/main/res/drawable/button_transparent.xml new file mode 100644 index 0000000..7e95a79 --- /dev/null +++ b/app/src/main/res/drawable/button_transparent.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/candidate.xml b/app/src/main/res/drawable/candidate.xml index 4f27963..38e276a 100644 --- a/app/src/main/res/drawable/candidate.xml +++ b/app/src/main/res/drawable/candidate.xml @@ -20,14 +20,16 @@ - - + + - - + + \ No newline at end of file diff --git a/app/src/main/res/drawable/clipboard.xml b/app/src/main/res/drawable/clipboard.xml new file mode 100644 index 0000000..745a35e --- /dev/null +++ b/app/src/main/res/drawable/clipboard.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/candidate.xml b/app/src/main/res/drawable/popup.xml similarity index 67% rename from app/src/main/res/layout/candidate.xml rename to app/src/main/res/drawable/popup.xml index 850e199..69aeff6 100644 --- a/app/src/main/res/layout/candidate.xml +++ b/app/src/main/res/drawable/popup.xml @@ -17,13 +17,8 @@ ~ --> - \ No newline at end of file + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/trashcan.xml b/app/src/main/res/drawable/trashcan.xml new file mode 100644 index 0000000..843eb65 --- /dev/null +++ b/app/src/main/res/drawable/trashcan.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/clip.xml b/app/src/main/res/layout/clip.xml new file mode 100644 index 0000000..7f55a07 --- /dev/null +++ b/app/src/main/res/layout/clip.xml @@ -0,0 +1,52 @@ + + + + +