mirror of
git://git.sv.gnu.org/emacs.git
synced 2026-01-09 13:10:57 -08:00
* java/org/gnu/emacs/EmacsActivity.java (onCreate): Permit overriding by child classes. (onDestroy): Minor stylistic adjustments. (getAttachmentToken): New function. * java/org/gnu/emacs/EmacsMultitaskActivity.java (onCreate) (getAttachmentToken): New functions. * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow): <attachmentToken, preserve, previouslyAttached>: New variables. (onActivityDetached): Remove redundant isFinishing argument. (reparentTo): Reset the foregoing fields before registering with the window manager. * java/org/gnu/emacs/EmacsWindowManager.java (EmacsWindowManager): Rename from EmacsWindowAttachmentManager. (WindowConsumer): New function getAttachmentToken. (isWindowEligible): New function. (registerWindowConsumer, registerWindow, removeWindowConsumer) (detachWindow): Implement a new window management strategy on API 29 and subsequent releases where both varieties of toplevel window are permanently, except when reparented, bound to the activities to which they attach, and Emacs establishes at strategic junctures whether those activities remain present. (getTaskToken, pruneWindows): New functions.
1885 lines
48 KiB
Java
1885 lines
48 KiB
Java
/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
|
||
|
||
Copyright (C) 2023-2024 Free Software Foundation, Inc.
|
||
|
||
This file is part of GNU Emacs.
|
||
|
||
GNU Emacs is free software: you can redistribute it and/or modify
|
||
it under the terms of the GNU General Public License as published by
|
||
the Free Software Foundation, either version 3 of the License, or (at
|
||
your option) any later version.
|
||
|
||
GNU Emacs is distributed in the hope that it will be useful,
|
||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
GNU General Public License for more details.
|
||
|
||
You should have received a copy of the GNU General Public License
|
||
along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
|
||
|
||
package org.gnu.emacs;
|
||
|
||
import java.lang.IllegalStateException;
|
||
import java.util.ArrayList;
|
||
import java.util.List;
|
||
import java.util.ListIterator;
|
||
import java.util.LinkedHashMap;
|
||
import java.util.Map;
|
||
|
||
import android.app.Activity;
|
||
|
||
import android.content.ClipData;
|
||
import android.content.ClipDescription;
|
||
import android.content.ContentResolver;
|
||
import android.content.Context;
|
||
|
||
import android.graphics.Rect;
|
||
import android.graphics.Canvas;
|
||
import android.graphics.Bitmap;
|
||
import android.graphics.PixelFormat;
|
||
|
||
import android.net.Uri;
|
||
|
||
import android.view.DragEvent;
|
||
import android.view.Gravity;
|
||
import android.view.InputDevice;
|
||
import android.view.KeyEvent;
|
||
import android.view.MotionEvent;
|
||
import android.view.View;
|
||
import android.view.ViewManager;
|
||
import android.view.WindowManager;
|
||
|
||
import android.util.SparseArray;
|
||
import android.util.Log;
|
||
|
||
import android.os.Build;
|
||
|
||
/* This defines a window, which is a handle. Windows represent a
|
||
rectangular subset of the screen with their own contents.
|
||
|
||
Windows either have a parent window, in which case their views are
|
||
attached to the parent's view, or are "floating", in which case
|
||
their views are attached to the parent activity (if any), else
|
||
nothing.
|
||
|
||
Views are also drawables, meaning they can accept drawing
|
||
requests. */
|
||
|
||
public final class EmacsWindow extends EmacsHandleObject
|
||
implements EmacsDrawable
|
||
{
|
||
private static final String TAG = "EmacsWindow";
|
||
|
||
private static class Coordinate
|
||
{
|
||
/* Integral coordinate. */
|
||
int x, y;
|
||
|
||
/* Button associated with the coordinate, or 0 if it is a touch
|
||
event. */
|
||
int button;
|
||
|
||
/* Pointer ID associated with the coordinate. */
|
||
int id;
|
||
|
||
public
|
||
Coordinate (int x, int y, int button, int id)
|
||
{
|
||
this.x = x;
|
||
this.y = y;
|
||
this.button = button;
|
||
this.id = id;
|
||
}
|
||
};
|
||
|
||
/* The view associated with the window. */
|
||
public EmacsView view;
|
||
|
||
/* The geometry of the window. */
|
||
private Rect rect;
|
||
|
||
/* The parent window, or null if it is the root window. */
|
||
public EmacsWindow parent;
|
||
|
||
/* List of all children in stacking order. This must be kept
|
||
consistent with their Z order!
|
||
|
||
Synchronize access to this list with itself. */
|
||
public ArrayList<EmacsWindow> children;
|
||
|
||
/* Map between pointer identifiers and last known position. Used to
|
||
compute which pointer changed upon a touch event. */
|
||
private SparseArray<Coordinate> pointerMap;
|
||
|
||
/* The window consumer currently attached, if it exists. */
|
||
private EmacsWindowManager.WindowConsumer attached;
|
||
|
||
/* The window background scratch GC. foreground is always the
|
||
window background. */
|
||
private EmacsGC scratchGC;
|
||
|
||
/* The button state and keyboard modifier mask at the time of the
|
||
last button press or release event. */
|
||
public int lastButtonState;
|
||
|
||
/* Whether or not the window is mapped. */
|
||
private volatile boolean isMapped;
|
||
|
||
/* Whether or not to ask for focus upon being mapped. */
|
||
private boolean dontFocusOnMap;
|
||
|
||
/* Whether or not the window is override-redirect. An
|
||
override-redirect window always has its own system window. */
|
||
private boolean overrideRedirect;
|
||
|
||
/* The window manager that is the parent of this window. NULL if
|
||
there is no such window manager. */
|
||
private WindowManager windowManager;
|
||
|
||
/* The time of the last KEYCODE_VOLUME_DOWN release. This is used
|
||
to quit Emacs upon two rapid clicks of the volume down
|
||
button. */
|
||
private long lastVolumeButtonRelease;
|
||
|
||
/* Linked list of character strings which were recently sent as
|
||
events. */
|
||
public LinkedHashMap<Integer, String> eventStrings;
|
||
|
||
/* Whether or not this window is fullscreen. */
|
||
public boolean fullscreen;
|
||
|
||
/* The window background pixel. This is used by EmacsView when
|
||
creating new bitmaps. */
|
||
public volatile int background;
|
||
|
||
/* The position of this window relative to the root window. */
|
||
public int xPosition, yPosition;
|
||
|
||
/* The position of the last drag and drop event received; both
|
||
values are -1 if no drag and drop operation is under way. */
|
||
private int dndXPosition, dndYPosition;
|
||
|
||
/* Identifier binding this window to the activity created for it, or
|
||
-1 if the window should be attached to system-created activities
|
||
(i.e. the activity launched by the system at startup). Value is
|
||
meaningless under API level 29 and earlier. */
|
||
public long attachmentToken;
|
||
|
||
/* Whether this window should be preserved during window pruning,
|
||
and whether this window has previously been attached to a task. */
|
||
public boolean preserve, previouslyAttached;
|
||
|
||
public
|
||
EmacsWindow (short handle, final EmacsWindow parent, int x, int y,
|
||
int width, int height, boolean overrideRedirect)
|
||
{
|
||
super (handle);
|
||
|
||
rect = new Rect (x, y, x + width, y + height);
|
||
pointerMap = new SparseArray<Coordinate> ();
|
||
|
||
/* Create the view from the context's UI thread. The window is
|
||
unmapped, so the view is GONE. */
|
||
view = EmacsService.SERVICE.getEmacsView (this, View.GONE,
|
||
parent == null);
|
||
this.parent = parent;
|
||
this.overrideRedirect = overrideRedirect;
|
||
|
||
/* Create the list of children. */
|
||
children = new ArrayList<EmacsWindow> ();
|
||
|
||
if (parent != null)
|
||
{
|
||
synchronized (parent.children)
|
||
{
|
||
parent.children.add (this);
|
||
}
|
||
|
||
EmacsService.SERVICE.runOnUiThread (new Runnable () {
|
||
@Override
|
||
public void
|
||
run ()
|
||
{
|
||
parent.view.addView (view);
|
||
}
|
||
});
|
||
}
|
||
|
||
scratchGC = new EmacsGC ((short) 0);
|
||
|
||
/* Create the map of input method-committed strings. Keep at most
|
||
ten strings in the map. */
|
||
|
||
eventStrings
|
||
= new LinkedHashMap<Integer, String> () {
|
||
@Override
|
||
protected boolean
|
||
removeEldestEntry (Map.Entry<Integer, String> entry)
|
||
{
|
||
return size () > 10;
|
||
}
|
||
};
|
||
|
||
dndXPosition = -1;
|
||
dndYPosition = -1;
|
||
}
|
||
|
||
public void
|
||
changeWindowBackground (int pixel)
|
||
{
|
||
/* scratchGC is used as the argument to a FillRectangles req. */
|
||
scratchGC.foreground = pixel;
|
||
scratchGC.markDirty (false);
|
||
|
||
/* Make the background known to the view as well. */
|
||
background = pixel;
|
||
}
|
||
|
||
public synchronized Rect
|
||
getGeometry ()
|
||
{
|
||
return new Rect (rect);
|
||
}
|
||
|
||
@Override
|
||
public synchronized void
|
||
destroyHandle () throws IllegalStateException
|
||
{
|
||
if (parent != null)
|
||
{
|
||
synchronized (parent.children)
|
||
{
|
||
parent.children.remove (this);
|
||
}
|
||
}
|
||
|
||
EmacsActivity.invalidateFocus (4);
|
||
|
||
if (!children.isEmpty ())
|
||
throw new IllegalStateException ("Trying to destroy window with "
|
||
+ "children!");
|
||
|
||
/* Remove the view from its parent and make it invisible. */
|
||
EmacsService.SERVICE.runOnUiThread (new Runnable () {
|
||
public void
|
||
run ()
|
||
{
|
||
ViewManager parent;
|
||
EmacsWindowManager manager;
|
||
|
||
if (EmacsActivity.focusedWindow == EmacsWindow.this)
|
||
EmacsActivity.focusedWindow = null;
|
||
|
||
manager = EmacsWindowManager.MANAGER;
|
||
view.setVisibility (View.GONE);
|
||
|
||
/* If the window manager is set, use that instead. */
|
||
if (windowManager != null)
|
||
parent = windowManager;
|
||
else
|
||
parent = (ViewManager) view.getParent ();
|
||
windowManager = null;
|
||
|
||
if (parent != null)
|
||
parent.removeView (view);
|
||
|
||
manager.detachWindow (EmacsWindow.this);
|
||
}
|
||
});
|
||
|
||
super.destroyHandle ();
|
||
}
|
||
|
||
public void
|
||
setConsumer (EmacsWindowManager.WindowConsumer consumer)
|
||
{
|
||
attached = consumer;
|
||
}
|
||
|
||
public EmacsWindowManager.WindowConsumer
|
||
getAttachedConsumer ()
|
||
{
|
||
return attached;
|
||
}
|
||
|
||
public synchronized long
|
||
viewLayout (int left, int top, int right, int bottom)
|
||
{
|
||
int rectWidth, rectHeight;
|
||
|
||
rect.left = left;
|
||
rect.top = top;
|
||
rect.right = right;
|
||
rect.bottom = bottom;
|
||
|
||
rectWidth = right - left;
|
||
rectHeight = bottom - top;
|
||
|
||
/* If parent is null, use xPosition and yPosition instead of the
|
||
geometry rectangle positions. */
|
||
|
||
if (parent == null)
|
||
{
|
||
left = xPosition;
|
||
top = yPosition;
|
||
}
|
||
|
||
return EmacsNative.sendConfigureNotify (this.handle,
|
||
System.currentTimeMillis (),
|
||
left, top, rectWidth,
|
||
rectHeight);
|
||
}
|
||
|
||
public void
|
||
requestViewLayout ()
|
||
{
|
||
view.explicitlyDirtyBitmap ();
|
||
|
||
EmacsService.SERVICE.runOnUiThread (new Runnable () {
|
||
@Override
|
||
public void
|
||
run ()
|
||
{
|
||
if (overrideRedirect)
|
||
/* Set the layout parameters again. */
|
||
view.setLayoutParams (getWindowLayoutParams ());
|
||
|
||
view.mustReportLayout = true;
|
||
view.requestLayout ();
|
||
}
|
||
});
|
||
}
|
||
|
||
public synchronized void
|
||
resizeWindow (int width, int height)
|
||
{
|
||
rect.right = rect.left + width;
|
||
rect.bottom = rect.top + height;
|
||
|
||
requestViewLayout ();
|
||
}
|
||
|
||
public synchronized void
|
||
moveWindow (int x, int y)
|
||
{
|
||
int width, height;
|
||
|
||
width = rect.width ();
|
||
height = rect.height ();
|
||
|
||
rect.left = x;
|
||
rect.top = y;
|
||
rect.right = x + width;
|
||
rect.bottom = y + height;
|
||
|
||
requestViewLayout ();
|
||
}
|
||
|
||
/* Return WM layout parameters for an override redirect window with
|
||
the geometry provided here. */
|
||
|
||
private WindowManager.LayoutParams
|
||
getWindowLayoutParams ()
|
||
{
|
||
WindowManager.LayoutParams params;
|
||
int flags, type;
|
||
Rect rect;
|
||
|
||
flags = 0;
|
||
rect = getGeometry ();
|
||
flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
|
||
flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
|
||
type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
|
||
|
||
params
|
||
= new WindowManager.LayoutParams (rect.width (), rect.height (),
|
||
rect.left, rect.top,
|
||
type, flags,
|
||
PixelFormat.RGBA_8888);
|
||
params.gravity = Gravity.TOP | Gravity.LEFT;
|
||
return params;
|
||
}
|
||
|
||
private Activity
|
||
findSuitableActivityContext ()
|
||
{
|
||
/* Find a recently focused activity. */
|
||
if (!EmacsActivity.focusedActivities.isEmpty ())
|
||
return EmacsActivity.focusedActivities.get (0);
|
||
|
||
/* Resort to the last activity to be focused. */
|
||
return EmacsActivity.lastFocusedActivity;
|
||
}
|
||
|
||
public synchronized void
|
||
mapWindow ()
|
||
{
|
||
final int width, height;
|
||
|
||
if (isMapped)
|
||
return;
|
||
|
||
isMapped = true;
|
||
width = rect.width ();
|
||
height = rect.height ();
|
||
|
||
if (parent == null)
|
||
{
|
||
EmacsService.SERVICE.runOnUiThread (new Runnable () {
|
||
@Override
|
||
public void
|
||
run ()
|
||
{
|
||
EmacsWindowManager manager;
|
||
WindowManager windowManager;
|
||
Activity ctx;
|
||
Object tem;
|
||
WindowManager.LayoutParams params;
|
||
|
||
/* Make the view visible, first of all. */
|
||
view.setVisibility (View.VISIBLE);
|
||
|
||
if (!overrideRedirect)
|
||
{
|
||
manager = EmacsWindowManager.MANAGER;
|
||
|
||
/* If parent is the root window, notice that there are new
|
||
children available for interested activities to pick
|
||
up. */
|
||
manager.registerWindow (EmacsWindow.this);
|
||
|
||
if (!getDontFocusOnMap ())
|
||
/* Eventually this should check no-focus-on-map. */
|
||
view.requestFocus ();
|
||
}
|
||
else
|
||
{
|
||
/* But if the window is an override-redirect window,
|
||
then:
|
||
|
||
- Find an activity that is currently active.
|
||
|
||
- Map the window as a panel on top of that
|
||
activity using the system window manager. */
|
||
|
||
ctx = findSuitableActivityContext ();
|
||
|
||
if (ctx == null)
|
||
{
|
||
Log.w (TAG, "failed to attach override-redirect window"
|
||
+ " for want of activity");
|
||
return;
|
||
}
|
||
|
||
tem = ctx.getSystemService (Context.WINDOW_SERVICE);
|
||
windowManager = (WindowManager) tem;
|
||
|
||
/* Calculate layout parameters and propagate the
|
||
activity's token into it. */
|
||
|
||
params = getWindowLayoutParams ();
|
||
params.token = (ctx.findViewById (android.R.id.content)
|
||
.getWindowToken ());
|
||
view.setLayoutParams (params);
|
||
|
||
/* Attach the view. */
|
||
try
|
||
{
|
||
view.prepareForLayout (width, height);
|
||
windowManager.addView (view, params);
|
||
|
||
/* Record the window manager being used in the
|
||
EmacsWindow object. */
|
||
EmacsWindow.this.windowManager = windowManager;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Log.w (TAG,
|
||
"failed to attach override-redirect window, " + e);
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
else
|
||
{
|
||
/* Do the same thing as above, but don't register this
|
||
window. */
|
||
EmacsService.SERVICE.runOnUiThread (new Runnable () {
|
||
@Override
|
||
public void
|
||
run ()
|
||
{
|
||
/* Prior to mapping the view, set its measuredWidth and
|
||
measuredHeight to some reasonable value, in order to
|
||
avoid excessive bitmap dirtying. */
|
||
|
||
view.prepareForLayout (width, height);
|
||
view.setVisibility (View.VISIBLE);
|
||
|
||
if (!getDontFocusOnMap ())
|
||
view.requestFocus ();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
public synchronized void
|
||
unmapWindow ()
|
||
{
|
||
if (!isMapped)
|
||
return;
|
||
|
||
isMapped = false;
|
||
|
||
view.post (new Runnable () {
|
||
@Override
|
||
public void
|
||
run ()
|
||
{
|
||
EmacsWindowManager manager;
|
||
|
||
manager = EmacsWindowManager.MANAGER;
|
||
|
||
view.setVisibility (View.GONE);
|
||
|
||
/* Detach the view from the window manager if possible. */
|
||
if (windowManager != null)
|
||
windowManager.removeView (view);
|
||
windowManager = null;
|
||
|
||
/* Now that the window is unmapped, unregister it as
|
||
well. */
|
||
manager.detachWindow (EmacsWindow.this);
|
||
}
|
||
});
|
||
}
|
||
|
||
@Override
|
||
public Canvas
|
||
lockCanvas (EmacsGC gc)
|
||
{
|
||
return view.getCanvas (gc);
|
||
}
|
||
|
||
@Override
|
||
public void
|
||
damageRect (Rect damageRect)
|
||
{
|
||
view.damageRect (damageRect.left,
|
||
damageRect.top,
|
||
damageRect.right,
|
||
damageRect.bottom);
|
||
}
|
||
|
||
@Override
|
||
public void
|
||
damageRect (int left, int top, int right, int bottom)
|
||
{
|
||
view.damageRect (left, top, right, bottom);
|
||
}
|
||
|
||
public void
|
||
swapBuffers ()
|
||
{
|
||
view.swapBuffers ();
|
||
}
|
||
|
||
public void
|
||
clearWindow ()
|
||
{
|
||
EmacsService.SERVICE.fillRectangle (this, scratchGC,
|
||
0, 0, rect.width (),
|
||
rect.height ());
|
||
}
|
||
|
||
public void
|
||
clearArea (int x, int y, int width, int height)
|
||
{
|
||
EmacsService.SERVICE.fillRectangle (this, scratchGC,
|
||
x, y, width, height);
|
||
}
|
||
|
||
@Override
|
||
public Bitmap
|
||
getBitmap ()
|
||
{
|
||
return view.getBitmap ();
|
||
}
|
||
|
||
/* event.getCharacters is used because older input methods still
|
||
require it. */
|
||
@SuppressWarnings ("deprecation")
|
||
public int
|
||
getEventUnicodeChar (KeyEvent event, int state)
|
||
{
|
||
String characters;
|
||
|
||
if (event.getUnicodeChar (state) != 0)
|
||
return event.getUnicodeChar (state);
|
||
|
||
characters = event.getCharacters ();
|
||
|
||
if (characters != null && characters.length () == 1)
|
||
return characters.charAt (0);
|
||
|
||
return characters == null ? 0 : -1;
|
||
}
|
||
|
||
public void
|
||
saveUnicodeString (int serial, String string)
|
||
{
|
||
eventStrings.put (serial, string);
|
||
}
|
||
|
||
|
||
|
||
/* Return the modifier mask associated with the specified keyboard
|
||
input EVENT. Replace bits corresponding to Left or Right keys
|
||
with their corresponding general modifier bits. */
|
||
|
||
public static int
|
||
eventModifiers (KeyEvent event)
|
||
{
|
||
int state;
|
||
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2)
|
||
state = event.getModifiers ();
|
||
else
|
||
{
|
||
/* Replace this with getMetaState and manual
|
||
normalization. */
|
||
state = event.getMetaState ();
|
||
|
||
/* Normalize the state by setting the generic modifier bit if
|
||
either a left or right modifier is pressed. */
|
||
|
||
if ((state & KeyEvent.META_ALT_LEFT_ON) != 0
|
||
|| (state & KeyEvent.META_ALT_RIGHT_ON) != 0)
|
||
state |= KeyEvent.META_ALT_MASK;
|
||
|
||
if ((state & KeyEvent.META_CTRL_LEFT_ON) != 0
|
||
|| (state & KeyEvent.META_CTRL_RIGHT_ON) != 0)
|
||
state |= KeyEvent.META_CTRL_MASK;
|
||
}
|
||
|
||
return state;
|
||
}
|
||
|
||
/* event.getCharacters is used because older input methods still
|
||
require it. */
|
||
@SuppressWarnings ("deprecation")
|
||
public void
|
||
onKeyDown (int keyCode, KeyEvent event)
|
||
{
|
||
int state, state_1, extra_ignored;
|
||
long serial;
|
||
String characters;
|
||
|
||
if (keyCode == KeyEvent.KEYCODE_BACK)
|
||
{
|
||
/* New Android systems display Back navigation buttons on a
|
||
row of virtual buttons at the bottom of the screen. These
|
||
buttons function much as physical buttons do, in that key
|
||
down events are produced when a finger taps them, even if
|
||
the finger is not ultimately released after the OS's
|
||
gesture navigation is activated.
|
||
|
||
Deliver onKeyDown events in onKeyUp instead, so as not to
|
||
navigate backwards during gesture navigation. */
|
||
|
||
return;
|
||
}
|
||
|
||
state = eventModifiers (event);
|
||
|
||
/* Num Lock, Scroll Lock and Meta aren't supported by systems older
|
||
than Android 3.0. */
|
||
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
|
||
extra_ignored = (KeyEvent.META_NUM_LOCK_ON
|
||
| KeyEvent.META_SCROLL_LOCK_ON
|
||
| KeyEvent.META_META_MASK);
|
||
else
|
||
extra_ignored = 0;
|
||
|
||
/* Ignore meta-state understood by Emacs for now, or key presses
|
||
such as Ctrl+C and Meta+C will not be recognized as ASCII key
|
||
press events. */
|
||
|
||
state_1
|
||
= state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK
|
||
| KeyEvent.META_SYM_ON | extra_ignored);
|
||
|
||
/* There's no distinction between Right Alt and Alt Gr on Android,
|
||
so restore META_ALT_RIGHT_ON if set in state to enable composing
|
||
characters. (bug#69321) */
|
||
|
||
if ((state & KeyEvent.META_ALT_RIGHT_ON) != 0)
|
||
{
|
||
state_1 |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON;
|
||
|
||
/* If Alt is also not depressed, remove its bit from the mask
|
||
reported to Emacs. */
|
||
if ((state & KeyEvent.META_ALT_LEFT_ON) == 0)
|
||
state &= ~KeyEvent.META_ALT_MASK;
|
||
}
|
||
|
||
synchronized (eventStrings)
|
||
{
|
||
serial
|
||
= EmacsNative.sendKeyPress (this.handle,
|
||
event.getEventTime (),
|
||
state, keyCode,
|
||
getEventUnicodeChar (event,
|
||
state_1));
|
||
|
||
characters = event.getCharacters ();
|
||
|
||
if (characters != null && characters.length () > 1)
|
||
saveUnicodeString ((int) serial, characters);
|
||
}
|
||
}
|
||
|
||
public void
|
||
onKeyUp (int keyCode, KeyEvent event)
|
||
{
|
||
int state, state_1, unicode_char, extra_ignored;
|
||
long time;
|
||
|
||
/* Compute the event's modifier mask. */
|
||
state = eventModifiers (event);
|
||
|
||
/* Num Lock, Scroll Lock and Meta aren't supported by systems older
|
||
than Android 3.0. */
|
||
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
|
||
extra_ignored = (KeyEvent.META_NUM_LOCK_ON
|
||
| KeyEvent.META_SCROLL_LOCK_ON
|
||
| KeyEvent.META_META_MASK);
|
||
else
|
||
extra_ignored = 0;
|
||
|
||
/* Ignore meta-state understood by Emacs for now, or key presses
|
||
such as Ctrl+C and Meta+C will not be recognized as ASCII key
|
||
press events. */
|
||
|
||
state_1
|
||
= state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK
|
||
| KeyEvent.META_SYM_ON | extra_ignored);
|
||
|
||
/* There's no distinction between Right Alt and Alt Gr on Android,
|
||
so restore META_ALT_RIGHT_ON if set in state to enable composing
|
||
characters. */
|
||
|
||
if ((state & KeyEvent.META_ALT_RIGHT_ON) != 0)
|
||
{
|
||
state_1 |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_RIGHT_ON;
|
||
|
||
/* If Alt is also not depressed, remove its bit from the mask
|
||
reported to Emacs. */
|
||
if ((state & KeyEvent.META_ALT_LEFT_ON) == 0)
|
||
state &= ~KeyEvent.META_ALT_MASK;
|
||
}
|
||
|
||
unicode_char = getEventUnicodeChar (event, state_1);
|
||
|
||
if (keyCode == KeyEvent.KEYCODE_BACK)
|
||
{
|
||
/* If the key press's been canceled, return immediately. */
|
||
|
||
if ((event.getFlags () & KeyEvent.FLAG_CANCELED) != 0)
|
||
return;
|
||
|
||
EmacsNative.sendKeyPress (this.handle, event.getEventTime (),
|
||
state, keyCode, unicode_char);
|
||
}
|
||
|
||
EmacsNative.sendKeyRelease (this.handle, event.getEventTime (),
|
||
state, keyCode, unicode_char);
|
||
|
||
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)
|
||
{
|
||
/* Check if this volume down press should quit Emacs.
|
||
Most Android devices have no physical keyboard, so it
|
||
is unreasonably hard to press C-g. */
|
||
|
||
time = event.getEventTime ();
|
||
|
||
if (time - lastVolumeButtonRelease < 350)
|
||
EmacsNative.quit ();
|
||
|
||
lastVolumeButtonRelease = time;
|
||
}
|
||
}
|
||
|
||
public void
|
||
onFocusChanged (boolean gainFocus)
|
||
{
|
||
EmacsActivity.invalidateFocus (gainFocus ? 6 : 5);
|
||
}
|
||
|
||
/* Notice that the activity (or its task) has been detached or
|
||
destroyed by explicit user action. */
|
||
|
||
public void
|
||
onActivityDetached ()
|
||
{
|
||
EmacsNative.sendWindowAction (this.handle, 0);
|
||
}
|
||
|
||
|
||
|
||
/* Mouse and touch event handling.
|
||
|
||
Android does not conceptually distinguish between mouse events
|
||
(those coming from a device whose movement affects the on-screen
|
||
pointer image) and touch screen events. Each click or touch
|
||
starts a single pointer gesture sequence, and subsequent motion
|
||
of the device will result in updates being reported relative to
|
||
that sequence until the mouse button or touch is released.
|
||
|
||
When a touch, click, or pointer motion takes place, several kinds
|
||
of event can be sent:
|
||
|
||
ACTION_DOWN or ACTION_POINTER_DOWN is sent with a new coordinate
|
||
and an associated ``pointer ID'' identifying the event and its
|
||
gesture sequence when a click or touch takes place. Emacs is
|
||
responsible for recording both the position and pointer ID of
|
||
this click for the purpose of determining future changes to its
|
||
position.
|
||
|
||
ACTION_UP or ACTION_POINTER_UP is sent with a pointer ID when the
|
||
click associated with a previous ACTION_DOWN event is released.
|
||
|
||
ACTION_CANCEL (or ACTION_POINTER_UP with FLAG_CANCELED) is sent
|
||
if a similar situation transpires: the window system has chosen
|
||
to grab the click, and future changes to its position will no
|
||
longer be reported to Emacs.
|
||
|
||
ACTION_MOVE is sent if a coordinate tied to a click that has not
|
||
been released changes. Emacs processes this event by comparing
|
||
each of the coordinates within the event with its recollection of
|
||
those contained within prior ACTION_DOWN and ACTION_MOVE events;
|
||
the pointer ID of the differing coordinate is then reported
|
||
within a touch or pointer motion event along with its new
|
||
position.
|
||
|
||
The events described above are all sent for both touch and mouse
|
||
click events. Determining whether an ACTION_DOWN event is
|
||
associated with a button event is performed by inspecting the
|
||
mouse button state associated with that event. If it contains
|
||
any mouse buttons that were not contained in the button state at
|
||
the time of the last ACTION_DOWN or ACTION_UP event, the
|
||
coordinate contained within is assumed to be a mouse click,
|
||
leading to it and associated motion or ACTION_UP events being
|
||
reported as mouse button or motion events. Otherwise, those
|
||
events are reported as touch screen events, with the touch ID set
|
||
to the pointer ID.
|
||
|
||
In addition to the events illustrated above, Android also sends
|
||
several other types of event upon select types of activity from a
|
||
mouse device:
|
||
|
||
ACTION_HOVER_MOVE is sent with the coordinate of the mouse
|
||
pointer if it moves above a frame prior to any click taking
|
||
place. Emacs sends a mouse motion event containing the
|
||
coordinate.
|
||
|
||
ACTION_HOVER_ENTER and ACTION_HOVER_LEAVE are respectively sent
|
||
when the mouse pointer enters and leaves a frame. Moreover,
|
||
ACTION_HOVER_LEAVE events are sent immediately before an
|
||
ACTION_DOWN event associated with a mouse click. These
|
||
extraneous events are distinct in that their button states always
|
||
contain an additional button compared to the button state
|
||
recorded at the time of the last ACTION_UP event.
|
||
|
||
On Android 6.0 and later, ACTION_BUTTON_PRESS is sent with the
|
||
coordinate of the mouse pointer if a mouse click occurs,
|
||
alongside a ACTION_DOWN event. ACTION_BUTTON_RELEASE is sent
|
||
with the same information upon a mouse click being released, also
|
||
accompanying an ACTION_UP event.
|
||
|
||
However, both types of button events are implemented in a buggy
|
||
fashion and cannot be used to report button events. */
|
||
|
||
/* Look through the button state to determine what button EVENT was
|
||
generated from. DOWN is true if EVENT is a button press event,
|
||
false otherwise. Value is the X number of the button. */
|
||
|
||
private int
|
||
whatButtonWasIt (MotionEvent event, boolean down)
|
||
{
|
||
int eventState, notIn;
|
||
|
||
/* Obtain the new button state. */
|
||
eventState = event.getButtonState ();
|
||
|
||
/* Compute which button is now set or no longer set. */
|
||
|
||
notIn = (down ? eventState & ~lastButtonState
|
||
: lastButtonState & ~eventState);
|
||
|
||
if ((notIn & (MotionEvent.BUTTON_PRIMARY
|
||
| MotionEvent.BUTTON_SECONDARY
|
||
| MotionEvent.BUTTON_TERTIARY)) == 0)
|
||
/* No buttons have been pressed, so this is a touch event. */
|
||
return 0;
|
||
|
||
if ((notIn & MotionEvent.BUTTON_PRIMARY) != 0)
|
||
return 1;
|
||
|
||
if ((notIn & MotionEvent.BUTTON_SECONDARY) != 0)
|
||
return 3;
|
||
|
||
if ((notIn & MotionEvent.BUTTON_TERTIARY) != 0)
|
||
return 2;
|
||
|
||
/* Buttons 4, 5, 6 and 7 are actually scroll wheels under X.
|
||
Thus, report additional buttons starting at 8. */
|
||
|
||
if ((notIn & MotionEvent.BUTTON_BACK) != 0)
|
||
return 8;
|
||
|
||
if ((notIn & MotionEvent.BUTTON_FORWARD) != 0)
|
||
return 9;
|
||
|
||
/* Report stylus events as touch screen events. */
|
||
|
||
if ((notIn & MotionEvent.BUTTON_STYLUS_PRIMARY) != 0)
|
||
return 0;
|
||
|
||
if ((notIn & MotionEvent.BUTTON_STYLUS_SECONDARY) != 0)
|
||
return 0;
|
||
|
||
/* Not a real value. */
|
||
return 11;
|
||
}
|
||
|
||
/* Return the mouse button associated with the specified ACTION_DOWN
|
||
or ACTION_POINTER_DOWN EVENT.
|
||
|
||
Value is 0 if no mouse button was pressed, or the X number of
|
||
that mouse button. */
|
||
|
||
private int
|
||
buttonForEvent (MotionEvent event)
|
||
{
|
||
/* ICS and earlier don't support true mouse button events, so
|
||
treat all down events as touch screen events. */
|
||
|
||
if (Build.VERSION.SDK_INT
|
||
< Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||
return 0;
|
||
|
||
return whatButtonWasIt (event, true);
|
||
}
|
||
|
||
/* Return the coordinate object associated with the specified
|
||
EVENT, or null if it is not known. */
|
||
|
||
private Coordinate
|
||
figureChange (MotionEvent event)
|
||
{
|
||
int i, truncatedX, truncatedY, pointerIndex, pointerID, count;
|
||
Coordinate coordinate;
|
||
|
||
/* Initialize this variable now. */
|
||
coordinate = null;
|
||
|
||
switch (event.getActionMasked ())
|
||
{
|
||
case MotionEvent.ACTION_DOWN:
|
||
/* Primary pointer pressed with index 0. */
|
||
|
||
pointerID = event.getPointerId (0);
|
||
coordinate = new Coordinate ((int) event.getX (0),
|
||
(int) event.getY (0),
|
||
buttonForEvent (event),
|
||
pointerID);
|
||
pointerMap.put (pointerID, coordinate);
|
||
break;
|
||
|
||
case MotionEvent.ACTION_UP:
|
||
case MotionEvent.ACTION_CANCEL:
|
||
/* Primary pointer released with index 0. */
|
||
pointerID = event.getPointerId (0);
|
||
coordinate = pointerMap.get (pointerID);
|
||
pointerMap.delete (pointerID);
|
||
break;
|
||
|
||
case MotionEvent.ACTION_POINTER_DOWN:
|
||
/* New pointer. Find the pointer ID from the index and place
|
||
it in the map. */
|
||
pointerIndex = event.getActionIndex ();
|
||
pointerID = event.getPointerId (pointerIndex);
|
||
coordinate = new Coordinate ((int) event.getX (pointerIndex),
|
||
(int) event.getY (pointerIndex),
|
||
buttonForEvent (event),
|
||
pointerID);
|
||
pointerMap.put (pointerID, coordinate);
|
||
break;
|
||
|
||
case MotionEvent.ACTION_POINTER_UP:
|
||
/* Pointer removed. Remove it from the map. */
|
||
pointerIndex = event.getActionIndex ();
|
||
pointerID = event.getPointerId (pointerIndex);
|
||
coordinate = pointerMap.get (pointerID);
|
||
pointerMap.delete (pointerID);
|
||
break;
|
||
|
||
default:
|
||
|
||
/* Loop through each pointer in the event. */
|
||
|
||
count = event.getPointerCount ();
|
||
for (i = 0; i < count; ++i)
|
||
{
|
||
pointerID = event.getPointerId (i);
|
||
|
||
/* Look up that pointer in the map. */
|
||
coordinate = pointerMap.get (pointerID);
|
||
|
||
if (coordinate != null)
|
||
{
|
||
/* See if coordinates have changed. */
|
||
truncatedX = (int) event.getX (i);
|
||
truncatedY = (int) event.getY (i);
|
||
|
||
if (truncatedX != coordinate.x
|
||
|| truncatedY != coordinate.y)
|
||
{
|
||
/* The pointer changed. Update the coordinate and
|
||
break out of the loop. */
|
||
coordinate.x = truncatedX;
|
||
coordinate.y = truncatedY;
|
||
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Set coordinate to NULL if the loop failed to find any
|
||
matching pointer. */
|
||
|
||
if (i == count)
|
||
coordinate = null;
|
||
}
|
||
|
||
/* Return the pointer ID. */
|
||
return coordinate;
|
||
}
|
||
|
||
/* Return the modifier mask associated with the specified motion
|
||
EVENT. Replace bits corresponding to Left or Right keys with
|
||
their corresponding general modifier bits. */
|
||
|
||
private int
|
||
motionEventModifiers (MotionEvent event)
|
||
{
|
||
int state;
|
||
|
||
state = event.getMetaState ();
|
||
|
||
/* Normalize the state by setting the generic modifier bit if
|
||
either a left or right modifier is pressed. */
|
||
|
||
if ((state & KeyEvent.META_ALT_LEFT_ON) != 0
|
||
|| (state & KeyEvent.META_ALT_RIGHT_ON) != 0)
|
||
state |= KeyEvent.META_ALT_MASK;
|
||
|
||
if ((state & KeyEvent.META_CTRL_LEFT_ON) != 0
|
||
|| (state & KeyEvent.META_CTRL_RIGHT_ON) != 0)
|
||
state |= KeyEvent.META_CTRL_MASK;
|
||
|
||
return state;
|
||
}
|
||
|
||
/* Process a single ACTION_DOWN, ACTION_POINTER_DOWN, ACTION_UP,
|
||
ACTION_POINTER_UP, ACTION_CANCEL, or ACTION_MOVE event.
|
||
|
||
Ascertain which coordinate changed and send an appropriate mouse
|
||
or touch screen event. */
|
||
|
||
private void
|
||
motionEvent (MotionEvent event)
|
||
{
|
||
Coordinate coordinate;
|
||
int modifiers;
|
||
long time;
|
||
|
||
/* Find data associated with this event's pointer. Namely, its
|
||
current location, whether or not a change has taken place, and
|
||
whether or not it is a button event. */
|
||
|
||
coordinate = figureChange (event);
|
||
|
||
if (coordinate == null)
|
||
return;
|
||
|
||
time = event.getEventTime ();
|
||
|
||
if (coordinate.button != 0)
|
||
{
|
||
/* This event is tied to a mouse click, so report mouse motion
|
||
and button events. */
|
||
|
||
modifiers = motionEventModifiers (event);
|
||
|
||
switch (event.getAction ())
|
||
{
|
||
case MotionEvent.ACTION_POINTER_DOWN:
|
||
case MotionEvent.ACTION_DOWN:
|
||
EmacsNative.sendButtonPress (this.handle, coordinate.x,
|
||
coordinate.y, time, modifiers,
|
||
coordinate.button);
|
||
break;
|
||
|
||
case MotionEvent.ACTION_POINTER_UP:
|
||
case MotionEvent.ACTION_UP:
|
||
case MotionEvent.ACTION_CANCEL:
|
||
EmacsNative.sendButtonRelease (this.handle, coordinate.x,
|
||
coordinate.y, time, modifiers,
|
||
coordinate.button);
|
||
break;
|
||
|
||
case MotionEvent.ACTION_MOVE:
|
||
EmacsNative.sendMotionNotify (this.handle, coordinate.x,
|
||
coordinate.y, time);
|
||
break;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
/* This event is a touch event, and the touch ID is the
|
||
pointer ID. */
|
||
|
||
switch (event.getActionMasked ())
|
||
{
|
||
case MotionEvent.ACTION_DOWN:
|
||
case MotionEvent.ACTION_POINTER_DOWN:
|
||
/* Touch down event. */
|
||
EmacsNative.sendTouchDown (this.handle, coordinate.x,
|
||
coordinate.y, time,
|
||
coordinate.id, 0);
|
||
break;
|
||
|
||
case MotionEvent.ACTION_UP:
|
||
case MotionEvent.ACTION_POINTER_UP:
|
||
/* Touch up event. */
|
||
EmacsNative.sendTouchUp (this.handle, coordinate.x,
|
||
coordinate.y, time,
|
||
coordinate.id, 0);
|
||
break;
|
||
|
||
case MotionEvent.ACTION_CANCEL:
|
||
/* Touch sequence cancellation event. */
|
||
EmacsNative.sendTouchUp (this.handle, coordinate.x,
|
||
coordinate.y, time,
|
||
coordinate.id,
|
||
1 /* ANDROID_TOUCH_SEQUENCE_CANCELED */);
|
||
break;
|
||
|
||
case MotionEvent.ACTION_MOVE:
|
||
/* Pointer motion event. */
|
||
EmacsNative.sendTouchMove (this.handle, coordinate.x,
|
||
coordinate.y, time,
|
||
coordinate.id, 0);
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (Build.VERSION.SDK_INT
|
||
< Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||
return;
|
||
|
||
/* Now update the button state. */
|
||
lastButtonState = event.getButtonState ();
|
||
return;
|
||
}
|
||
|
||
public boolean
|
||
onTouchEvent (MotionEvent event)
|
||
{
|
||
switch (event.getActionMasked ())
|
||
{
|
||
case MotionEvent.ACTION_DOWN:
|
||
case MotionEvent.ACTION_POINTER_DOWN:
|
||
case MotionEvent.ACTION_UP:
|
||
case MotionEvent.ACTION_POINTER_UP:
|
||
case MotionEvent.ACTION_CANCEL:
|
||
case MotionEvent.ACTION_MOVE:
|
||
motionEvent (event);
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
public boolean
|
||
onGenericMotionEvent (MotionEvent event)
|
||
{
|
||
switch (event.getAction ())
|
||
{
|
||
case MotionEvent.ACTION_HOVER_ENTER:
|
||
EmacsNative.sendEnterNotify (this.handle, (int) event.getX (),
|
||
(int) event.getY (),
|
||
event.getEventTime ());
|
||
return true;
|
||
|
||
case MotionEvent.ACTION_HOVER_MOVE:
|
||
EmacsNative.sendMotionNotify (this.handle, (int) event.getX (),
|
||
(int) event.getY (),
|
||
event.getEventTime ());
|
||
return true;
|
||
|
||
case MotionEvent.ACTION_HOVER_EXIT:
|
||
|
||
/* If the exit event comes from a button press, its button
|
||
state will have extra bits compared to the last known
|
||
button state. Since the exit event will interfere with
|
||
tool bar button presses, ignore such splurious events. */
|
||
|
||
if ((event.getButtonState () & ~lastButtonState) == 0)
|
||
EmacsNative.sendLeaveNotify (this.handle, (int) event.getX (),
|
||
(int) event.getY (),
|
||
event.getEventTime ());
|
||
|
||
return true;
|
||
|
||
case MotionEvent.ACTION_DOWN:
|
||
case MotionEvent.ACTION_POINTER_DOWN:
|
||
case MotionEvent.ACTION_UP:
|
||
case MotionEvent.ACTION_POINTER_UP:
|
||
case MotionEvent.ACTION_CANCEL:
|
||
case MotionEvent.ACTION_MOVE:
|
||
/* MotionEvents may either be sent to onGenericMotionEvent or
|
||
onTouchEvent depending on if Android thinks it is a mouse
|
||
event or not, but we detect them ourselves. */
|
||
motionEvent (event);
|
||
return true;
|
||
|
||
case MotionEvent.ACTION_SCROLL:
|
||
/* Send a scroll event with the specified deltas. */
|
||
EmacsNative.sendWheel (this.handle, (int) event.getX (),
|
||
(int) event.getY (),
|
||
event.getEventTime (),
|
||
motionEventModifiers (event),
|
||
event.getAxisValue (MotionEvent.AXIS_HSCROLL),
|
||
event.getAxisValue (MotionEvent.AXIS_VSCROLL));
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
|
||
|
||
public synchronized void
|
||
reparentTo (final EmacsWindow otherWindow, int x, int y)
|
||
{
|
||
int width, height;
|
||
|
||
/* Reparent this window to the other window. */
|
||
|
||
if (parent != null)
|
||
{
|
||
synchronized (parent.children)
|
||
{
|
||
parent.children.remove (this);
|
||
}
|
||
}
|
||
|
||
if (otherWindow != null)
|
||
{
|
||
synchronized (otherWindow.children)
|
||
{
|
||
otherWindow.children.add (this);
|
||
}
|
||
}
|
||
|
||
parent = otherWindow;
|
||
|
||
/* Move this window to the new location. */
|
||
width = rect.width ();
|
||
height = rect.height ();
|
||
rect.left = x;
|
||
rect.top = y;
|
||
rect.right = x + width;
|
||
rect.bottom = y + height;
|
||
|
||
/* Now do the work necessary on the UI thread to reparent the
|
||
window. */
|
||
EmacsService.SERVICE.runOnUiThread (new Runnable () {
|
||
@Override
|
||
public void
|
||
run ()
|
||
{
|
||
EmacsWindowManager manager;
|
||
ViewManager parent;
|
||
|
||
/* First, detach this window if necessary. */
|
||
manager = EmacsWindowManager.MANAGER;
|
||
manager.detachWindow (EmacsWindow.this);
|
||
|
||
/* Reset window management state. */
|
||
previouslyAttached = false;
|
||
attachmentToken = false;
|
||
|
||
/* Also unparent this view. */
|
||
|
||
/* If the window manager is set, use that instead. */
|
||
if (windowManager != null)
|
||
parent = windowManager;
|
||
else
|
||
parent = (ViewManager) view.getParent ();
|
||
windowManager = null;
|
||
|
||
if (parent != null)
|
||
parent.removeView (view);
|
||
|
||
/* Next, either add this window as a child of the new
|
||
parent's view, or make it available again. */
|
||
if (otherWindow != null)
|
||
otherWindow.view.addView (view);
|
||
else if (EmacsWindow.this.isMapped)
|
||
manager.registerWindow (EmacsWindow.this);
|
||
|
||
/* Request relayout. */
|
||
view.requestLayout ();
|
||
}
|
||
});
|
||
}
|
||
|
||
public void
|
||
makeInputFocus (long time)
|
||
{
|
||
/* TIME is currently ignored. Request the input focus now. */
|
||
|
||
EmacsService.SERVICE.runOnUiThread (new Runnable () {
|
||
@Override
|
||
public void
|
||
run ()
|
||
{
|
||
view.requestFocus ();
|
||
}
|
||
});
|
||
}
|
||
|
||
public synchronized void
|
||
raise ()
|
||
{
|
||
/* This does nothing here. */
|
||
if (parent == null)
|
||
return;
|
||
|
||
synchronized (parent.children)
|
||
{
|
||
/* Remove and add this view again. */
|
||
parent.children.remove (this);
|
||
parent.children.add (this);
|
||
}
|
||
|
||
/* Request a relayout. */
|
||
EmacsService.SERVICE.runOnUiThread (new Runnable () {
|
||
@Override
|
||
public void
|
||
run ()
|
||
{
|
||
view.raise ();
|
||
}
|
||
});
|
||
}
|
||
|
||
public synchronized void
|
||
lower ()
|
||
{
|
||
/* This does nothing here. */
|
||
if (parent == null)
|
||
return;
|
||
|
||
synchronized (parent.children)
|
||
{
|
||
/* Remove and add this view again. */
|
||
parent.children.remove (this);
|
||
parent.children.add (this);
|
||
}
|
||
|
||
/* Request a relayout. */
|
||
EmacsService.SERVICE.runOnUiThread (new Runnable () {
|
||
@Override
|
||
public void
|
||
run ()
|
||
{
|
||
view.lower ();
|
||
}
|
||
});
|
||
}
|
||
|
||
public synchronized void
|
||
reconfigure (final EmacsWindow window, final int stackMode)
|
||
{
|
||
ListIterator<EmacsWindow> iterator;
|
||
EmacsWindow object;
|
||
|
||
/* This does nothing here. */
|
||
if (parent == null)
|
||
return;
|
||
|
||
/* If window is NULL, call lower or upper subject to
|
||
stackMode. */
|
||
|
||
if (window == null)
|
||
{
|
||
if (stackMode == 1) /* ANDROID_BELOW */
|
||
lower ();
|
||
else
|
||
raise ();
|
||
|
||
return;
|
||
}
|
||
|
||
/* Otherwise, if window.parent is distinct from this, return. */
|
||
if (window.parent != this.parent)
|
||
return;
|
||
|
||
/* Synchronize with the parent's child list. Iterate over each
|
||
item until WINDOW is encountered, before moving this window to
|
||
the location prescribed by STACKMODE. */
|
||
|
||
synchronized (parent.children)
|
||
{
|
||
/* Remove this window from parent.children, for it will be
|
||
reinserted before or after WINDOW. */
|
||
parent.children.remove (this);
|
||
|
||
/* Create an iterator. */
|
||
iterator = parent.children.listIterator ();
|
||
|
||
while (iterator.hasNext ())
|
||
{
|
||
object = iterator.next ();
|
||
|
||
if (object == window)
|
||
{
|
||
/* Now place this before or after the cursor of the
|
||
iterator. */
|
||
|
||
if (stackMode == 0) /* ANDROID_ABOVE */
|
||
iterator.add (this);
|
||
else
|
||
{
|
||
iterator.previous ();
|
||
iterator.add (this);
|
||
}
|
||
|
||
/* Effect the same adjustment upon the view
|
||
hierarchy. */
|
||
|
||
EmacsService.SERVICE.runOnUiThread (new Runnable () {
|
||
@Override
|
||
public void
|
||
run ()
|
||
{
|
||
if (stackMode == 0)
|
||
view.moveAbove (window.view);
|
||
else
|
||
view.moveBelow (window.view);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/* parent.children does not list WINDOW, which should never
|
||
transpire. */
|
||
EmacsNative.emacsAbort ();
|
||
}
|
||
}
|
||
|
||
public synchronized int[]
|
||
getWindowGeometry ()
|
||
{
|
||
int[] array;
|
||
|
||
array = new int[4];
|
||
|
||
array[0] = parent != null ? rect.left : xPosition;
|
||
array[1] = parent != null ? rect.top : yPosition;
|
||
array[2] = rect.width ();
|
||
array[3] = rect.height ();
|
||
|
||
return array;
|
||
}
|
||
|
||
public void
|
||
noticeIconified ()
|
||
{
|
||
EmacsNative.sendIconified (this.handle);
|
||
}
|
||
|
||
public void
|
||
noticeDeiconified ()
|
||
{
|
||
EmacsNative.sendDeiconified (this.handle);
|
||
}
|
||
|
||
public synchronized void
|
||
setDontAcceptFocus (final boolean dontAcceptFocus)
|
||
{
|
||
/* Update the view's focus state. */
|
||
EmacsService.SERVICE.runOnUiThread (new Runnable () {
|
||
@Override
|
||
public void
|
||
run ()
|
||
{
|
||
view.setFocusable (!dontAcceptFocus);
|
||
view.setFocusableInTouchMode (!dontAcceptFocus);
|
||
}
|
||
});
|
||
}
|
||
|
||
public synchronized void
|
||
setDontFocusOnMap (final boolean dontFocusOnMap)
|
||
{
|
||
this.dontFocusOnMap = dontFocusOnMap;
|
||
}
|
||
|
||
public synchronized boolean
|
||
getDontFocusOnMap ()
|
||
{
|
||
return dontFocusOnMap;
|
||
}
|
||
|
||
public int[]
|
||
translateCoordinates (int x, int y)
|
||
{
|
||
int[] array;
|
||
|
||
/* This is supposed to translate coordinates to the root
|
||
window. */
|
||
array = new int[2];
|
||
EmacsService.SERVICE.getLocationOnScreen (view, array);
|
||
|
||
/* Now, the coordinates of the view should be in array. Offset X
|
||
and Y by them. */
|
||
array[0] += x;
|
||
array[1] += y;
|
||
|
||
/* Return the resulting coordinates. */
|
||
return array;
|
||
}
|
||
|
||
public void
|
||
toggleOnScreenKeyboard (final boolean on)
|
||
{
|
||
/* Even though InputMethodManager functions are thread safe,
|
||
`showOnScreenKeyboard' etc must be called from the UI thread in
|
||
order to avoid deadlocks if the calls happen in tandem with a
|
||
call to a synchronizing function within
|
||
`onCreateInputConnection'. */
|
||
|
||
EmacsService.SERVICE.runOnUiThread (new Runnable () {
|
||
@Override
|
||
public void
|
||
run ()
|
||
{
|
||
if (on)
|
||
view.showOnScreenKeyboard ();
|
||
else
|
||
view.hideOnScreenKeyboard ();
|
||
}
|
||
});
|
||
}
|
||
|
||
public String
|
||
lookupString (int eventSerial)
|
||
{
|
||
String any;
|
||
|
||
synchronized (eventStrings)
|
||
{
|
||
any = eventStrings.remove (eventSerial);
|
||
}
|
||
|
||
return any;
|
||
}
|
||
|
||
public void
|
||
setFullscreen (final boolean isFullscreen)
|
||
{
|
||
EmacsService.SERVICE.runOnUiThread (new Runnable () {
|
||
@Override
|
||
public void
|
||
run ()
|
||
{
|
||
EmacsActivity activity;
|
||
Object tem;
|
||
|
||
fullscreen = isFullscreen;
|
||
tem = getAttachedConsumer ();
|
||
|
||
if (tem != null)
|
||
{
|
||
activity = (EmacsActivity) tem;
|
||
activity.syncFullscreenWith (EmacsWindow.this);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
public void
|
||
defineCursor (final EmacsCursor cursor)
|
||
{
|
||
/* Don't post this message if pointer icons aren't supported. */
|
||
|
||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||
view.post (new Runnable () {
|
||
@Override
|
||
public void
|
||
run ()
|
||
{
|
||
if (cursor != null)
|
||
view.setPointerIcon (cursor.icon);
|
||
else
|
||
view.setPointerIcon (null);
|
||
}
|
||
});
|
||
}
|
||
|
||
public synchronized void
|
||
notifyContentRectPosition (int xPosition, int yPosition)
|
||
{
|
||
Rect geometry;
|
||
|
||
/* Ignore these notifications if not a child of the root
|
||
window. */
|
||
if (parent != null)
|
||
return;
|
||
|
||
/* xPosition and yPosition are the position of this window
|
||
relative to the screen. Set them and request a ConfigureNotify
|
||
event. */
|
||
|
||
if (this.xPosition != xPosition
|
||
|| this.yPosition != yPosition)
|
||
{
|
||
this.xPosition = xPosition;
|
||
this.yPosition = yPosition;
|
||
|
||
EmacsNative.sendConfigureNotify (this.handle,
|
||
System.currentTimeMillis (),
|
||
xPosition, yPosition,
|
||
rect.width (), rect.height ());
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/* Drag and drop.
|
||
|
||
Android 7.0 and later permit multiple windows to be juxtaposed
|
||
on-screen, consequently enabling items selected from one window
|
||
to be dragged onto another. Data is transferred across program
|
||
boundaries using ClipData items, much the same way clipboard data
|
||
is transferred.
|
||
|
||
When an item is dropped, Emacs must ascertain whether the clip
|
||
data represents plain text, a content URI incorporating a file,
|
||
or some other data. This is implemented by examining the clip
|
||
data's ``description'', which enumerates each of the MIME data
|
||
types the clip data is capable of providing data in.
|
||
|
||
If the clip data represents plain text, then that text is copied
|
||
into a string and conveyed to Lisp code. Otherwise, Emacs must
|
||
solicit rights to access the URI from the system, absent which it
|
||
is accounted plain text and reinterpreted as such, to cue the
|
||
user that something has gone awry.
|
||
|
||
Moreover, events are regularly sent as the item being dragged
|
||
travels across the frame, even if it might not be dropped. This
|
||
facilitates cursor motion and scrolling in response, as provided
|
||
by the options dnd-indicate-insertion-point and
|
||
dnd-scroll-margin. */
|
||
|
||
/* Register the drag and drop event EVENT. */
|
||
|
||
public boolean
|
||
onDragEvent (DragEvent event)
|
||
{
|
||
ClipData data;
|
||
ClipDescription description;
|
||
int i, j, x, y, itemCount;
|
||
String type, uriString;
|
||
Uri uri;
|
||
EmacsActivity activity;
|
||
StringBuilder builder;
|
||
ContentResolver resolver;
|
||
|
||
x = (int) event.getX ();
|
||
y = (int) event.getY ();
|
||
|
||
switch (event.getAction ())
|
||
{
|
||
case DragEvent.ACTION_DRAG_STARTED:
|
||
/* Return true to continue the drag and drop operation. */
|
||
return true;
|
||
|
||
case DragEvent.ACTION_DRAG_LOCATION:
|
||
/* Send this drag motion event to Emacs. Skip this when the
|
||
integer position hasn't changed, for Android sends events
|
||
even if the movement from the previous position of the drag
|
||
is less than 1 pixel on either axis. */
|
||
|
||
if (x != dndXPosition || y != dndYPosition)
|
||
{
|
||
EmacsNative.sendDndDrag (handle, x, y);
|
||
dndXPosition = x;
|
||
dndYPosition = y;
|
||
}
|
||
|
||
return true;
|
||
|
||
case DragEvent.ACTION_DROP:
|
||
/* Reset this view's record of the previous drag and drop
|
||
event's position. */
|
||
dndXPosition = -1;
|
||
dndYPosition = -1;
|
||
|
||
/* Judge whether this is plain text, or if it's a file URI for
|
||
which permissions must be requested. */
|
||
|
||
data = event.getClipData ();
|
||
description = data.getDescription ();
|
||
itemCount = data.getItemCount ();
|
||
|
||
/* If there are insufficient items within the clip data,
|
||
return false. */
|
||
|
||
if (itemCount < 1)
|
||
return false;
|
||
|
||
/* Search for plain text data within the clipboard. */
|
||
|
||
for (i = 0; i < description.getMimeTypeCount (); ++i)
|
||
{
|
||
type = description.getMimeType (i);
|
||
|
||
if (type.equals (ClipDescription.MIMETYPE_TEXT_PLAIN)
|
||
|| type.equals (ClipDescription.MIMETYPE_TEXT_HTML))
|
||
{
|
||
/* The data being dropped is plain text; encode it
|
||
suitably and send it to the main thread. */
|
||
type = (data.getItemAt (0).coerceToText (EmacsService.SERVICE)
|
||
.toString ());
|
||
EmacsNative.sendDndText (handle, x, y, type);
|
||
return true;
|
||
}
|
||
else if (type.equals (ClipDescription.MIMETYPE_TEXT_URILIST))
|
||
{
|
||
/* The data being dropped is a list of URIs; encode it
|
||
suitably and send it to the main thread. */
|
||
type = (data.getItemAt (0).coerceToText (EmacsService.SERVICE)
|
||
.toString ());
|
||
EmacsNative.sendDndUri (handle, x, y, type);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/* There's no plain text data within this clipboard item, so
|
||
each item within should be treated as a content URI
|
||
designating a file. */
|
||
|
||
/* Collect the URIs into a string with each suffixed
|
||
by newlines, much as in a text/uri-list. */
|
||
builder = new StringBuilder ();
|
||
|
||
for (i = 0; i < itemCount; ++i)
|
||
{
|
||
/* If the item dropped is a URI, send it to the
|
||
main thread. */
|
||
|
||
uri = data.getItemAt (i).getUri ();
|
||
|
||
/* Attempt to acquire permissions for this URI;
|
||
failing which, insert it as text instead. */
|
||
|
||
if (uri != null
|
||
&& uri.getScheme () != null
|
||
&& uri.getScheme ().equals ("content")
|
||
&& (activity = EmacsActivity.lastFocusedActivity) != null)
|
||
{
|
||
if ((activity.requestDragAndDropPermissions (event) == null))
|
||
uri = null;
|
||
else
|
||
{
|
||
resolver = activity.getContentResolver ();
|
||
|
||
/* Substitute a content file name for the URI, if
|
||
possible. */
|
||
uriString = EmacsService.buildContentName (uri, resolver);
|
||
|
||
if (uriString != null)
|
||
{
|
||
builder.append (uriString).append ("\n");
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (uri != null)
|
||
builder.append (uri.toString ()).append ("\n");
|
||
else
|
||
{
|
||
/* Treat each URI that Emacs cannot secure
|
||
permissions for as plain text. */
|
||
type = (data.getItemAt (i)
|
||
.coerceToText (EmacsService.SERVICE)
|
||
.toString ());
|
||
EmacsNative.sendDndText (handle, x, y, type);
|
||
}
|
||
}
|
||
|
||
/* Now send each URI to Emacs. */
|
||
|
||
if (builder.length () > 0)
|
||
EmacsNative.sendDndUri (handle, x, y, builder.toString ());
|
||
return true;
|
||
|
||
default:
|
||
/* Reset this view's record of the previous drag and drop
|
||
event's position. */
|
||
dndXPosition = -1;
|
||
dndYPosition = -1;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
|
||
|
||
/* Miscellaneous functions for debugging graphics code. */
|
||
|
||
/* Recreate the activity to which this window is attached, if any.
|
||
This is nonfunctional on Android 2.3.7 and earlier. */
|
||
|
||
public void
|
||
recreateActivity ()
|
||
{
|
||
final EmacsWindowManager.WindowConsumer attached;
|
||
|
||
attached = this.attached;
|
||
|
||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB)
|
||
return;
|
||
|
||
view.post (new Runnable () {
|
||
@Override
|
||
public void
|
||
run ()
|
||
{
|
||
if (attached instanceof EmacsActivity)
|
||
((EmacsActivity) attached).recreate ();
|
||
}
|
||
});
|
||
}
|
||
};
|