diff --git a/res/drawable/btn_floating_toggle.xml b/res/drawable/btn_floating_toggle.xml
new file mode 100644
index 000000000..5c2c386e9
--- /dev/null
+++ b/res/drawable/btn_floating_toggle.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/res/drawable/ic_floating_toggle.xml b/res/drawable/ic_floating_toggle.xml
new file mode 100644
index 000000000..524103404
--- /dev/null
+++ b/res/drawable/ic_floating_toggle.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/res/drawable/ic_floating_toggle_selected.xml b/res/drawable/ic_floating_toggle_selected.xml
new file mode 100644
index 000000000..6e183caea
--- /dev/null
+++ b/res/drawable/ic_floating_toggle_selected.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/res/layout/keyboard.xml b/res/layout/keyboard.xml
index 6af865946..b11882310 100644
--- a/res/layout/keyboard.xml
+++ b/res/layout/keyboard.xml
@@ -1,11 +1,13 @@
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6c416523e..cc9e014f0 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -47,6 +47,8 @@
Double tap on shift for caps lock
You can lock any modifier by holding it
Behavior
+ Floating keyboard
+ Show the keyboard as a small movable window
Automatic capitalisation
Press Shift at the beginning of a sentence
Switching between input methods
@@ -162,6 +164,7 @@
Download failed: Please allow internet access
Click to install a dictionary for %s
Change language
+ Toggle floating keyboard
When a physical keyboard is connected
Hide everything
Show everything
diff --git a/res/xml/settings.xml b/res/xml/settings.xml
index 63f894aaa..c0c17f339 100644
--- a/res/xml/settings.xml
+++ b/res/xml/settings.xml
@@ -28,6 +28,7 @@
+
diff --git a/srcs/juloo.keyboard2/Config.java b/srcs/juloo.keyboard2/Config.java
index fd19b49cc..354e0b60b 100644
--- a/srcs/juloo.keyboard2/Config.java
+++ b/srcs/juloo.keyboard2/Config.java
@@ -25,6 +25,13 @@ public final class Config
*/
public static final int WIDE_DEVICE_THRESHOLD = 600;
+ /** Ratio of the screen width used by the floating keyboard window. */
+ public static final float FLOATING_KEYBOARD_WIDTH_RATIO = 0.7f;
+ /** Maximum width of the floating keyboard window, in dp. */
+ public static final int FLOATING_KEYBOARD_MAX_WIDTH_DP = 700;
+ /** Height scale applied to the rows of the keyboard in floating mode. */
+ public static final float FLOATING_KEYBOARD_HEIGHT_SCALE = 0.75f;
+
private final SharedPreferences _prefs;
// From resources
@@ -76,6 +83,7 @@ public final class Config
public int clipboard_history_duration;
public boolean space_bar_auto_complete;
public boolean physical_keyboard_hide;
+ public boolean floating_keyboard;
// Dynamically set
/** Configuration options implied by the connected editor. */
@@ -180,7 +188,10 @@ public void refresh(Resources res, Boolean foldableUnfolded, Dictionaries dicts)
// The keyboard is keyboardHeightPercent of the screen height on 16/9
// screens (or less) and with a 3.95 high layout (in KeyboardData unit)
float base_height = Math.min(dm.heightPixels, dm.widthPixels * 16.f / 9.f);
+ floating_keyboard = _prefs.getBoolean("floating_keyboard", false);
keyboard_rows_height_pixels = (int)(base_height * keyboardHeightPercent / 395);
+ if (floating_keyboard)
+ keyboard_rows_height_pixels *= FLOATING_KEYBOARD_HEIGHT_SCALE;
horizontal_margin =
get_dip_pref_oriented(dm, "horizontal_margin", 3, 28);
double_tap_lock_shift = _prefs.getBoolean("lock_double_tap", false);
diff --git a/srcs/juloo.keyboard2/FloatingHandleView.java b/srcs/juloo.keyboard2/FloatingHandleView.java
new file mode 100644
index 000000000..af1297f18
--- /dev/null
+++ b/srcs/juloo.keyboard2/FloatingHandleView.java
@@ -0,0 +1,51 @@
+package juloo.keyboard2;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.View;
+
+/** A small grabber handle used to drag the floating keyboard window. */
+public class FloatingHandleView extends View
+{
+ private static final float HANDLE_WIDTH_DP = 32;
+ private static final float HANDLE_THICKNESS_DP = 3;
+ private static final float HANDLE_SPACING_DP = 6;
+ private static final int HANDLE_ALPHA = 120;
+
+ private final Paint _paint;
+
+ public FloatingHandleView(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ _paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ _paint.setStyle(Paint.Style.STROKE);
+ _paint.setStrokeCap(Paint.Cap.ROUND);
+ _paint.setStrokeWidth(dp(context, HANDLE_THICKNESS_DP));
+ TypedArray a = context.getTheme().obtainStyledAttributes(R.styleable.keyboard);
+ _paint.setColor(a.getColor(R.styleable.keyboard_colorLabel, 0));
+ a.recycle();
+ _paint.setAlpha(HANDLE_ALPHA);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas)
+ {
+ float width = dp(getContext(), HANDLE_WIDTH_DP);
+ float spacing = dp(getContext(), HANDLE_SPACING_DP);
+ float cx = getWidth() / 2.f;
+ float cy = getHeight() / 2.f;
+ for (int i = -1; i <= 1; i++)
+ {
+ float y = cy + i * spacing;
+ canvas.drawLine(cx - width / 2.f, y, cx + width / 2.f, y, _paint);
+ }
+ }
+
+ private static float dp(Context context, float value)
+ {
+ return value * context.getResources().getDisplayMetrics().density;
+ }
+}
diff --git a/srcs/juloo.keyboard2/KeyValue.java b/srcs/juloo.keyboard2/KeyValue.java
index 6cfce92f7..dbc024918 100644
--- a/srcs/juloo.keyboard2/KeyValue.java
+++ b/srcs/juloo.keyboard2/KeyValue.java
@@ -26,6 +26,7 @@ public static enum Event
SWITCH_VOICE_TYPING_CHOOSER,
HIDE_SELF,
CHANGE_DICTIONARY,
+ TOGGLE_FLOATING,
}
// Must be evaluated in the reverse order of their values.
@@ -663,6 +664,7 @@ public static KeyValue getSpecialKeyByName(String name)
case "complete_emoji": return statefulKey(Stateful.Complete_emoji);
case "hide_self": return eventKey("⊻", Event.HIDE_SELF, FLAG_SMALLER_FONT);
case "change_dictionary": return eventKey(0xE01D, Event.CHANGE_DICTIONARY, 0);
+ case "toggle_floating": return eventKey("Float", Event.TOGGLE_FLOATING, FLAG_SMALLER_FONT);
/* 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 1ecfb4b5d..0e3ff3555 100644
--- a/srcs/juloo.keyboard2/Keyboard2.java
+++ b/srcs/juloo.keyboard2/Keyboard2.java
@@ -4,6 +4,7 @@
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
+import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.inputmethodservice.InputMethodService;
import android.os.Build.VERSION;
@@ -36,6 +37,12 @@
public class Keyboard2 extends InputMethodService
implements SharedPreferences.OnSharedPreferenceChangeListener
{
+ /** Preference keys storing the last floating window position. */
+ private static final String PREF_FLOATING_X = "floating_x";
+ private static final String PREF_FLOATING_Y = "floating_y";
+ /** Fraction of the screen height used as the default top offset. */
+ private static final float DEFAULT_FLOATING_Y_RATIO = 0.25f;
+
/** The view containing the keyboard and candidates view. */
private ViewGroup _keyboard_container_view;
private Keyboard2View _keyboard_layout_view;
@@ -56,6 +63,12 @@ public class Keyboard2 extends InputMethodService
private FoldStateTracker _foldStateTracker;
+ /** Drag state of the floating keyboard handle. */
+ private float _dragStartX;
+ private float _dragStartY;
+ private int _windowStartX;
+ private int _windowStartY;
+
/** Layout currently visible before it has been modified. */
KeyboardData current_layout_unmodified()
{
@@ -162,6 +175,8 @@ private void create_keyboard_view()
_keyboard_container_view = (ViewGroup)inflate_view(R.layout.keyboard);
_keyboard_layout_view = (Keyboard2View)_keyboard_container_view.findViewById(R.id.keyboard_view);
_candidates_view = (CandidatesView)_keyboard_container_view.findViewById(R.id.candidates_view);
+ View handle = _keyboard_container_view.findViewById(R.id.floating_handle);
+ handle.setOnTouchListener(_floatingHandleTouchListener);
}
InputMethodManager get_imm()
@@ -240,9 +255,30 @@ private void refresh_config()
bg.setAlpha(_config.keyboardOpacity);
_keyboard_container_view.setBackground(bg);
_keyboard_layout_view.reset();
+ updateFloatingHandleVisibility();
refresh_candidates_view();
}
+ /** Show or hide the floating keyboard drag handle. */
+ private void updateFloatingHandleVisibility()
+ {
+ View handle = _keyboard_container_view.findViewById(R.id.floating_handle);
+ if (handle != null)
+ handle.setVisibility(_config.floating_keyboard ? View.VISIBLE : View.GONE);
+ }
+
+ /** Toggle floating keyboard mode on and off from the keyboard. */
+ private void toggleFloatingKeyboard()
+ {
+ Config.globalPrefs().edit()
+ .putBoolean("floating_keyboard", !_config.floating_keyboard)
+ .apply();
+ refresh_config();
+ _keyboard_layout_view.setKeyboard(current_layout());
+ updateSoftInputWindowLayoutParams();
+ updateFloatingHandleVisibility();
+ }
+
private KeyboardData refresh_special_layout()
{
if (_config.editor_config.numeric_layout)
@@ -289,12 +325,27 @@ public void updateFullscreenMode() {
}
private void updateSoftInputWindowLayoutParams() {
+ if (_config.floating_keyboard)
+ {
+ applyFloatingWindow();
+ return;
+ }
final Window window = getWindow().getWindow();
+ // Restore the docked layout when leaving floating mode
+ WindowManager.LayoutParams wattrs = window.getAttributes();
+ if (wattrs.width != ViewGroup.LayoutParams.MATCH_PARENT
+ || wattrs.gravity != Gravity.BOTTOM)
+ {
+ wattrs.width = ViewGroup.LayoutParams.MATCH_PARENT;
+ wattrs.gravity = Gravity.BOTTOM;
+ wattrs.x = 0;
+ wattrs.y = 0;
+ window.setAttributes(wattrs);
+ }
// On API >= 35, Keyboard2View behaves as edge-to-edge
// APIs 30 to 34 have visual artifact when edge-to-edge is enabled
if (VERSION.SDK_INT >= 35)
{
- WindowManager.LayoutParams wattrs = window.getAttributes();
wattrs.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
// Allow to draw behind system bars
@@ -313,6 +364,107 @@ private void updateSoftInputWindowLayoutParams() {
}
+ /** Make the IME window a small floating window at the stored position. */
+ private void applyFloatingWindow()
+ {
+ if (!_config.floating_keyboard)
+ return;
+ final Window window = getWindow().getWindow();
+ final WindowManager.LayoutParams lp = window.getAttributes();
+ Point pos = getFloatingPosition();
+ lp.gravity = Gravity.TOP | Gravity.START;
+ lp.width = floatingWindowWidth();
+ lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+ lp.x = pos.x;
+ lp.y = pos.y;
+ window.setAttributes(lp);
+ }
+
+ /** Width of the floating keyboard window, in pixels. */
+ private int floatingWindowWidth()
+ {
+ android.util.DisplayMetrics dm = getResources().getDisplayMetrics();
+ return (int)Math.min(dm.widthPixels * Config.FLOATING_KEYBOARD_WIDTH_RATIO,
+ dm.density * Config.FLOATING_KEYBOARD_MAX_WIDTH_DP);
+ }
+
+ /** Position of the floating window, loaded from the preferences or a
+ default position (centered horizontally, ~25% from the top). */
+ private Point getFloatingPosition()
+ {
+ SharedPreferences prefs = Config.globalPrefs();
+ if (prefs.contains(PREF_FLOATING_X) && prefs.contains(PREF_FLOATING_Y))
+ return new Point(prefs.getInt(PREF_FLOATING_X, 0),
+ prefs.getInt(PREF_FLOATING_Y, 0));
+ android.util.DisplayMetrics dm = getResources().getDisplayMetrics();
+ int x = (dm.widthPixels - floatingWindowWidth()) / 2;
+ int y = (int)(dm.heightPixels * DEFAULT_FLOATING_Y_RATIO);
+ return new Point(x, y);
+ }
+
+ @Override
+ public void onConfigureWindow(Window window, boolean isFullscreen,
+ boolean isCandidatesOnly)
+ {
+ super.onConfigureWindow(window, isFullscreen, isCandidatesOnly);
+ applyFloatingWindow();
+ }
+
+ @Override
+ public void onComputeInsets(Insets outInsets)
+ {
+ super.onComputeInsets(outInsets);
+ // Report zero insets in floating mode so the app behind is not resized.
+ if (_config.floating_keyboard)
+ {
+ outInsets.contentTopInsets = 0;
+ outInsets.visibleTopInsets = 0;
+ }
+ }
+
+ /** Drag the floating window from its handle. */
+ private final View.OnTouchListener _floatingHandleTouchListener =
+ new View.OnTouchListener()
+ {
+ @Override
+ public boolean onTouch(View v, MotionEvent event)
+ {
+ switch (event.getActionMasked())
+ {
+ case MotionEvent.ACTION_DOWN:
+ _dragStartX = event.getRawX();
+ _dragStartY = event.getRawY();
+ WindowManager.LayoutParams lp = getWindow().getWindow().getAttributes();
+ _windowStartX = lp.x;
+ _windowStartY = lp.y;
+ v.getParent().requestDisallowInterceptTouchEvent(true);
+ return true;
+ case MotionEvent.ACTION_MOVE:
+ WindowManager wm = getWindow().getWindow().getWindowManager();
+ WindowManager.LayoutParams lp2 = getWindow().getWindow().getAttributes();
+ int dx = (int)(event.getRawX() - _dragStartX);
+ int dy = (int)(event.getRawY() - _dragStartY);
+ int winW = getWindow().getWindow().getDecorView().getWidth();
+ int winH = getWindow().getWindow().getDecorView().getHeight();
+ android.util.DisplayMetrics dm = getResources().getDisplayMetrics();
+ lp2.x = Math.max(0, Math.min(_windowStartX + dx, dm.widthPixels - winW));
+ lp2.y = Math.max(0, Math.min(_windowStartY + dy, dm.heightPixels - winH));
+ wm.updateViewLayout(getWindow().getWindow().getDecorView(), lp2);
+ return true;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ WindowManager.LayoutParams lp3 = getWindow().getWindow().getAttributes();
+ Config.globalPrefs().edit()
+ .putInt(PREF_FLOATING_X, lp3.x)
+ .putInt(PREF_FLOATING_Y, lp3.y)
+ .apply();
+ v.getParent().requestDisallowInterceptTouchEvent(false);
+ return true;
+ }
+ return false;
+ }
+ };
+
private static void updateLayoutHeightOf(final Window window, final int layoutHeight) {
final WindowManager.LayoutParams params = window.getAttributes();
if (params != null && params.height != layoutHeight) {
@@ -376,6 +528,8 @@ public void onSharedPreferenceChanged(SharedPreferences _prefs, String _key)
{
refresh_config();
_keyboard_layout_view.setKeyboard(current_layout());
+ updateSoftInputWindowLayoutParams();
+ updateFloatingHandleVisibility();
}
@Override
@@ -515,6 +669,10 @@ public void handle_event_key(KeyValue.Event ev)
case CHANGE_DICTIONARY:
new DictionarySwitcher(Keyboard2.this, _dictionaries, this).choose();
break;
+
+ case TOGGLE_FLOATING:
+ toggleFloatingKeyboard();
+ break;
}
}
diff --git a/srcs/juloo.keyboard2/Keyboard2View.java b/srcs/juloo.keyboard2/Keyboard2View.java
index 5e854a0ff..0c2352bb5 100644
--- a/srcs/juloo.keyboard2/Keyboard2View.java
+++ b/srcs/juloo.keyboard2/Keyboard2View.java
@@ -265,7 +265,16 @@ private void vibrate()
public void onMeasure(int wSpec, int hSpec)
{
DisplayMetrics dm = getContext().getResources().getDisplayMetrics();
- int width = dm.widthPixels;
+ int width;
+ if (_config.floating_keyboard)
+ {
+ width = (int)Math.min(dm.widthPixels * Config.FLOATING_KEYBOARD_WIDTH_RATIO,
+ dm.density * Config.FLOATING_KEYBOARD_MAX_WIDTH_DP);
+ }
+ else
+ {
+ width = dm.widthPixels;
+ }
_marginLeft = Math.max(_config.horizontal_margin, _insets_left);
_marginRight = Math.max(_config.horizontal_margin, _insets_right);
_marginBottom = _config.margin_bottom + _insets_bottom;
diff --git a/srcs/juloo.keyboard2/suggestions/CandidatesView.java b/srcs/juloo.keyboard2/suggestions/CandidatesView.java
index adb0d2433..c7dbb61da 100644
--- a/srcs/juloo.keyboard2/suggestions/CandidatesView.java
+++ b/srcs/juloo.keyboard2/suggestions/CandidatesView.java
@@ -39,6 +39,8 @@ public class CandidatesView extends LinearLayout
View _dictionary_switch_button;
boolean should_show_dictionary_switch = false;
+ View _floating_toggle_button;
+
public CandidatesView(Context context, AttributeSet attrs)
{
super(context, attrs);
@@ -53,6 +55,7 @@ protected void onFinishInflate()
setup_item_view(2, R.id.candidates_left);
setup_item_view(3, R.id.candidates_emoji);
setup_dictionary_switch_button();
+ setup_floating_toggle_button();
}
public void set_candidates(Suggestions s)
@@ -101,6 +104,8 @@ public void refresh_config(Config config)
else if (_status_no_dict != null)
_status_no_dict.setVisibility(View.GONE);
should_show_dictionary_switch = config.should_show_dictionary_switch;
+ _floating_toggle_button.setSelected(config.floating_keyboard);
+ _floating_toggle_button.setVisibility(View.VISIBLE);
set_sizes(config);
}
@@ -177,6 +182,21 @@ public void onClick(View _v)
});
}
+ void setup_floating_toggle_button()
+ {
+ _floating_toggle_button = findViewById(R.id.floating_toggle);
+ _floating_toggle_button.setOnClickListener(new View.OnClickListener()
+ {
+ @Override
+ public void onClick(View _v)
+ {
+ Config.globalConfig().handler.key_up(
+ KeyValue.getKeyByName("toggle_floating"),
+ Pointers.Modifiers.EMPTY);
+ }
+ });
+ }
+
/** Whether the candidates view should be shown for a given editor. */
public static boolean should_show(EditorInfo info)
{