Clipboard history pane

Work in progress: It's not yet possible to paste from the pane.

The pane can be switched to and from and displays the strings recently
added to the clipboard.

ClipboardHistoryService listens for change to the system clipboard and
keep the history in memory.
This data is not persisted to the storage.

The maximum size limits the amount of user data stored in memory but
also gives a sense to the user that the history is not persisted and can
be forgotten as soon as the app stops.
This commit is contained in:
Jules Aguillon 2024-01-14 00:50:33 +01:00
parent 45fc18576e
commit 58cb6ca232
29 changed files with 243 additions and 2 deletions

View File

@ -6,7 +6,8 @@ warning_count = 0
KNOWN_NOT_LAYOUT = set([
"number_row", "numpad", "pin",
"bottom_row", "settings", "method",
"greekmath", "numeric", "emoji_bottom_row" ])
"greekmath", "numeric", "emoji_bottom_row",
"clipboard_bottom_row" ])
def warn(msg):
global warning_count

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:background="?attr/colorKeyboard" android:hardwareAccelerated="false">
<TextView android:text="@string/clipboard_history_heading" style="@style/clipboardHeading" android:layout_width="fill_parent" android:layout_height="wrap_content"/>
<juloo.keyboard2.ClipboardHistoryView android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="@dimen/clipboard_view_height" android:divider="?attr/clipboard_divider_color" android:dividerHeight="?attr/clipboard_divider_height"/>
<juloo.keyboard2.Keyboard2View layout="@xml/clipboard_bottom_row" android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="?attr/colorKeyboard"/>
</LinearLayout>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="wrap_content">
<TextView android:id="@android:id/text1" style="@style/clipboardEntry" android:layout_width="fill_parent" android:layout_height="wrap_content"/>
</LinearLayout>

View File

@ -115,4 +115,5 @@ Tato aplikace neobsahuje žádné reklamy, nevyužívá připojení k síti a je
<string name="key_descr_page_down">Page Down</string>
<string name="key_descr_home">Home</string>
<string name="key_descr_end">End</string>
<!-- <string name="clipboard_history_heading">Recently copied text</string> -->
</resources>

View File

@ -115,4 +115,5 @@ Diese App enthält keine Werbung, benötigt keinen Netzwerkzugriff und ist quell
<string name="key_descr_page_down">Bild ab</string>
<string name="key_descr_home">Pos1</string>
<string name="key_descr_end">Ende</string>
<!-- <string name="clipboard_history_heading">Recently copied text</string> -->
</resources>

View File

@ -115,4 +115,5 @@ La misma no contiene ningún anuncio/publicidad, no realiza peticiones de red y
<string name="key_descr_page_down">Re Pág</string>
<string name="key_descr_home">Inicio</string>
<string name="key_descr_end">Fin</string>
<!-- <string name="clipboard_history_heading">Recently copied text</string> -->
</resources>

View File

@ -115,4 +115,5 @@ This application contains no ads, doesn't make any network requests and is Open
<!-- <string name="key_descr_page_down">Page Down</string> -->
<!-- <string name="key_descr_home">Home</string> -->
<!-- <string name="key_descr_end">End</string> -->
<!-- <string name="clipboard_history_heading">Recently copied text</string> -->
</resources>

View File

@ -115,4 +115,5 @@ Cette application ne contient pas de publicité, n'accède pas au réseau et est
<string name="key_descr_page_down">Page suivante</string>
<string name="key_descr_home">Début</string>
<string name="key_descr_end">Fin</string>
<!-- <string name="clipboard_history_heading">Recently copied text</string> -->
</resources>

View File

@ -115,4 +115,5 @@ This application contains no ads, doesn't make any network requests and is Open
<!-- <string name="key_descr_page_down">Page Down</string> -->
<!-- <string name="key_descr_home">Home</string> -->
<!-- <string name="key_descr_end">End</string> -->
<!-- <string name="clipboard_history_heading">Recently copied text</string> -->
</resources>

View File

@ -115,4 +115,5 @@ This application contains no ads, doesn't make any network requests and is Open
<!-- <string name="key_descr_page_down">Page Down</string> -->
<!-- <string name="key_descr_home">Home</string> -->
<!-- <string name="key_descr_end">End</string> -->
<!-- <string name="clipboard_history_heading">Recently copied text</string> -->
</resources>

View File

@ -117,4 +117,5 @@ Tagad lieliski piemērota izmantošanai ikdienā.
<string name="key_descr_page_down">Lejupšķirt</string>
<string name="key_descr_home">Sākums</string>
<string name="key_descr_end">Beigas</string>
<!-- <string name="clipboard_history_heading">Recently copied text</string> -->
</resources>

View File

@ -115,4 +115,5 @@ Aplikacja nie zawiera reklam, nie żąda dostępu do internetu, a jej kod źród
<string name="key_descr_page_down">Page Down</string>
<string name="key_descr_home">Home</string>
<string name="key_descr_end">End</string>
<!-- <string name="clipboard_history_heading">Recently copied text</string> -->
</resources>

View File

@ -115,4 +115,5 @@ Este aplicativo não contém anúncios, não faz nenhuma solicitação de rede e
<string name="key_descr_page_down">Page Down</string>
<string name="key_descr_home">Home</string>
<string name="key_descr_end">End</string>
<!-- <string name="clipboard_history_heading">Recently copied text</string> -->
</resources>

View File

@ -115,4 +115,5 @@ Această aplicație nu conține publicitate, nu folosește rețeaua deloc și e
<!-- <string name="key_descr_page_down">Page Down</string> -->
<!-- <string name="key_descr_home">Home</string> -->
<!-- <string name="key_descr_end">End</string> -->
<!-- <string name="clipboard_history_heading">Recently copied text</string> -->
</resources>

View File

@ -115,4 +115,5 @@
<string name="key_descr_page_down">Страница вниз</string>
<string name="key_descr_home">Home</string>
<string name="key_descr_end">End</string>
<!-- <string name="clipboard_history_heading">Recently copied text</string> -->
</resources>

View File

@ -115,4 +115,5 @@ Bu uygulama açık kaynaklıdır. Reklam içermez ve internete bağlanmaz."</str
<string name="key_descr_page_down">Aşağı</string>
<string name="key_descr_home">BAŞ(Sol yön tuşu)</string>
<string name="key_descr_end">SON(Sağ yön tuşu)</string>
<!-- <string name="clipboard_history_heading">Recently copied text</string> -->
</resources>

View File

@ -115,4 +115,5 @@
<string name="key_descr_page_down">Page Down</string>
<string name="key_descr_home">Home</string>
<string name="key_descr_end">End</string>
<!-- <string name="clipboard_history_heading">Recently copied text</string> -->
</resources>

View File

@ -115,4 +115,5 @@ Bây giờ đã hoàn hảo cho việc sử dụng hàng ngày.
<!-- <string name="key_descr_page_down">Page Down</string> -->
<!-- <string name="key_descr_home">Home</string> -->
<!-- <string name="key_descr_end">End</string> -->
<!-- <string name="clipboard_history_heading">Recently copied text</string> -->
</resources>

View File

@ -115,4 +115,5 @@
<string name="key_descr_page_down">下一页</string>
<string name="key_descr_home">Home</string>
<string name="key_descr_end">End</string>
<!-- <string name="clipboard_history_heading">Recently copied text</string> -->
</resources>

View File

@ -115,4 +115,5 @@ This application contains no ads, doesn't make any network requests and is Open
<string name="key_descr_page_down">Page Down</string>
<string name="key_descr_home">Home</string>
<string name="key_descr_end">End</string>
<string name="clipboard_history_heading">Recently copied text</string>
</resources>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Emoji pane -->
<style name="emojiTypeButton">
<item name="android:padding">1px</item>
<item name="android:gravity">center</item>
@ -15,6 +16,22 @@
<item name="android:textSize">@dimen/emoji_text_size</item>
<item name="android:textColor">?attr/emoji_color</item>
</style>
<!-- Clipboard pane -->
<style name="clipboardEntry">
<item name="android:layout_marginHorizontal">14dp</item>
<item name="android:layout_marginVertical">14dp</item>
<item name="android:textSize">16dp</item>
<item name="android:textColor">?attr/colorLabel</item>
</style>
<style name="clipboardHeading">
<item name="android:layout_marginHorizontal">6dp</item>
<item name="android:layout_marginTop">14dp</item>
<item name="android:layout_marginBottom">0dp</item>
<item name="android:textSize">14dp</item>
<item name="android:fontWeight">700</item>
<item name="android:textColor">?attr/colorSubLabel</item>
</style>
<!-- Launcher activity -->
<style name="paragraph">
<item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item>

View File

@ -23,11 +23,14 @@
<attr name="keyBorderColorTop" format="color"/>
<attr name="keyBorderColorRight" format="color"/>
<attr name="keyBorderColorBottom" format="color"/>
<!-- Emoji panel -->
<!-- Emoji pane -->
<attr name="emoji_button_bg" type="color" format="color"/>
<attr name="emoji_color" type="color" format="color"/>
<attr name="emoji_key_bg" type="color" format="color"/>
<attr name="emoji_key_text" type="color" format="color"/>
<!-- Clipboard pane -->
<attr name="clipboard_divider_color" type="color" format="color"/>
<attr name="clipboard_divider_height" format="dimension"/>
<!-- System integration -->
<attr name="navigationBarColor" format="color"/>
<attr name="windowLightNavigationBar" format="boolean"/>
@ -43,6 +46,8 @@
<item name="greyedDimming">0.5</item>
<item name="emoji_key_bg" type="color">?attr/emoji_button_bg</item>
<item name="emoji_key_text" type="color">?attr/colorLabel</item>
<item name="clipboard_divider_color" type="color">?attr/colorKey</item>
<item name="clipboard_divider_height">1px</item>
</style>
<style name="Dark" parent="BaseTheme">
<item name="android:isLightTheme">false</item>
@ -116,6 +121,7 @@
<item name="colorSubLabel">#333333</item>
<item name="emoji_button_bg">#ffffff</item>
<item name="emoji_color">#000000</item>
<item name="clipboard_divider_color" type="color">#eeeeee</item>
</style>
<style name="ePaper" parent="BaseTheme">
<item name="android:isLightTheme">true</item>
@ -134,6 +140,8 @@
<item name="colorSubLabel">#333333</item>
<item name="emoji_button_bg">#ffffff</item>
<item name="emoji_color">#000000</item>
<item name="clipboard_divider_color" type="color">#000000</item>
<item name="clipboard_divider_height">2dp</item>
</style>
<style name="Desert" parent="@style/BaseTheme">
<item name="android:isLightTheme">true</item>

View File

@ -4,6 +4,7 @@
<dimen name="key_padding">2dp</dimen>
<dimen name="emoji_grid_height">250dp</dimen>
<dimen name="emoji_text_size">28dp</dimen>
<dimen name="clipboard_view_height">200dp</dimen>
<dimen name="pref_button_size">28dp</dimen>
<bool name="debug_logs">false</bool> <!-- Will be overwritten automatically by Gradle for the debug build variant -->
</resources>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The bottom row used in the clipboard history pane. -->
<keyboard bottom_row="false">
<row height="0.95">
<key key0="switch_back_emoji"/>
<key shift="4" key0="backspace"/>
<key key0="enter"/>
</row>
</keyboard>

View File

@ -0,0 +1,85 @@
package juloo.keyboard2;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Build.VERSION;
import java.util.ArrayList;
import java.util.List;
public final class ClipboardHistoryService
{
/** Start the service on startup and start listening to clipboard changes. */
public static void on_startup(Context ctx)
{
get_service(ctx);
}
/** Start the service if it hasn't been started before. Returns [null] if the
feature is unsupported. */
public static ClipboardHistoryService get_service(Context ctx)
{
if (VERSION.SDK_INT <= 11)
return null;
if (_service == null)
_service = new ClipboardHistoryService(ctx);
return _service;
}
/** The maximum size limits the amount of user data stored in memory but also
gives a sense to the user that the history is not persisted and can be
forgotten as soon as the app stops. */
public static final int MAX_HISTORY_SIZE = 3;
static ClipboardHistoryService _service = null;
ClipboardManager _cm;
List<String> _history;
OnClipboardHistoryChange _listener = null;
ClipboardHistoryService(Context ctx)
{
_history = new ArrayList<String>();
_cm = (ClipboardManager)ctx.getSystemService(Context.CLIPBOARD_SERVICE);
_cm.addPrimaryClipChangedListener(this.new SystemListener());
}
public List<String> get_history() { return _history; }
/** Add clipboard entries to the history, skipping consecutive duplicates and
empty strings. */
public void add_clip(String clip)
{
int size = _history.size();
if (clip.equals("") || (size > 0 && _history.get(size - 1).equals(clip)))
return;
if (size >= MAX_HISTORY_SIZE)
_history.remove(0);
_history.add(clip);
if (_listener != null)
_listener.on_clipboard_history_change(_history);
}
public void set_on_clipboard_history_change(OnClipboardHistoryChange l) { _listener = l; }
public static interface OnClipboardHistoryChange
{
public void on_clipboard_history_change(List<String> history);
}
final class SystemListener implements ClipboardManager.OnPrimaryClipChangedListener
{
public SystemListener() {}
@Override
public void onPrimaryClipChanged()
{
ClipData clip = _cm.getPrimaryClip();
if (clip == null)
return;
int count = clip.getItemCount();
for (int i = 0; i < count; i++)
add_clip(clip.getItemAt(i).getText().toString());
}
}
}

View File

@ -0,0 +1,78 @@
package juloo.keyboard2;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class ClipboardHistoryView extends ListView
implements ClipboardHistoryService.OnClipboardHistoryChange
{
List<String> _history;
ClipboardEntriesAdapter _adapter;
public ClipboardHistoryView(Context ctx, AttributeSet attrs)
{
super(ctx, attrs);
_history = Collections.EMPTY_LIST;
_adapter = this.new ClipboardEntriesAdapter();
ClipboardHistoryService service = ClipboardHistoryService.get_service(ctx);
if (service != null)
{
service.set_on_clipboard_history_change(this);
_history = service.get_history();
}
setAdapter(_adapter);
}
@Override
public void on_clipboard_history_change(List<String> history)
{
_history = history;
_adapter.notifyDataSetChanged();
invalidate();
}
@Override
protected void onAttachedToWindow()
{
super.onAttachedToWindow();
_adapter.notifyDataSetChanged();
}
class ClipboardEntriesAdapter extends BaseAdapter
{
public ClipboardEntriesAdapter() {}
@Override
public int getCount()
{
return _history.size();
}
public Object getItem(int pos)
{
return _history.get(pos);
}
public long getItemId(int pos)
{
return _history.get(pos).hashCode();
}
@Override
public View getView(int pos, View v, ViewGroup _parent)
{
if (v == null)
v = View.inflate(getContext(), R.layout.clipboard_pane_entry, null);
((TextView)v.findViewById(android.R.id.text1)).setText(_history.get(pos));
return v;
}
}
}

View File

@ -12,6 +12,8 @@ public final class KeyValue implements Comparable<KeyValue>
SWITCH_NUMERIC,
SWITCH_EMOJI,
SWITCH_BACK_EMOJI,
SWITCH_CLIPBOARD,
SWITCH_BACK_CLIPBOARD,
CHANGE_METHOD_PICKER,
CHANGE_METHOD_AUTO,
ACTION,
@ -460,6 +462,8 @@ public final class KeyValue implements Comparable<KeyValue>
case "switch_numeric": return eventKey("123+", Event.SWITCH_NUMERIC, FLAG_SMALLER_FONT);
case "switch_emoji": return eventKey(0xE001, Event.SWITCH_EMOJI, FLAG_SMALLER_FONT);
case "switch_back_emoji": return eventKey("ABC", Event.SWITCH_BACK_EMOJI, 0);
case "switch_clipboard": return eventKey("Clip", Event.SWITCH_CLIPBOARD, FLAG_SMALLER_FONT);
case "switch_back_clipboard": return eventKey("ABC", Event.SWITCH_BACK_CLIPBOARD, 0);
case "switch_forward": return eventKey(0xE013, Event.SWITCH_FORWARD, FLAG_SMALLER_FONT);
case "switch_backward": return eventKey(0xE014, Event.SWITCH_BACKWARD, FLAG_SMALLER_FONT);
case "switch_greekmath": return eventKey("πλ∇¬", Event.SWITCH_GREEKMATH, FLAG_SMALLER_FONT);

View File

@ -36,6 +36,7 @@ public class Keyboard2 extends InputMethodService
/** Layout associated with the currently selected locale. Not 'null'. */
private KeyboardData _localeTextLayout;
private ViewGroup _emojiPane = null;
private ViewGroup _clipboard_pane = null;
public int actionId; // Action performed by the Action key.
private Config _config;
@ -113,6 +114,7 @@ public class Keyboard2 extends InputMethodService
_keyboardView = (Keyboard2View)inflate_view(R.layout.keyboard);
_keyboardView.reset();
Logs.set_debug_logs(getResources().getBoolean(R.bool.debug_logs));
ClipboardHistoryService.on_startup(this);
}
private List<InputMethodSubtype> getEnabledSubtypes(InputMethodManager imm)
@ -223,6 +225,7 @@ public class Keyboard2 extends InputMethodService
{
_keyboardView = (Keyboard2View)inflate_view(R.layout.keyboard);
_emojiPane = null;
_clipboard_pane = null;
setInputView(_keyboardView);
}
_keyboardView.reset();
@ -384,7 +387,14 @@ public class Keyboard2 extends InputMethodService
setInputView(_emojiPane);
break;
case SWITCH_CLIPBOARD:
if (_clipboard_pane == null)
_clipboard_pane = (ViewGroup)inflate_view(R.layout.clipboard_pane);
setInputView(_clipboard_pane);
break;
case SWITCH_BACK_EMOJI:
case SWITCH_BACK_CLIPBOARD:
setInputView(_keyboardView);
break;

View File

@ -24,6 +24,7 @@ public class ExtraKeysPreference extends PreferenceCategory
"meta",
"compose",
"voice_typing",
"switch_clipboard",
"accent_aigu",
"accent_grave",
"accent_double_aigu",