emacs-gpu/java/org/gnu/emacs/EmacsView.java
Po Lu 2b87ab7b27 Update Android port
* java/Makefile.in (clean): Fix distclean and bootstrap-clean rules.
* java/debug.sh (jdb_port):
(attach_existing):
(num_pids):
(line): Add new options to upload a gdbserver binary to the device.

* java/org/gnu/emacs/EmacsActivity.java (EmacsActivity): Make
focusedActivities public.
* java/org/gnu/emacs/EmacsContextMenu.java (EmacsContextMenu):
New class.
* java/org/gnu/emacs/EmacsDrawRectangle.java (perform): Fix
bounds computation.
* java/org/gnu/emacs/EmacsGC.java (markDirty): Set stroke width
explicitly.
* java/org/gnu/emacs/EmacsService.java (EmacsService)
(getLocationOnScreen, nameKeysym): New functions.
* java/org/gnu/emacs/EmacsView.java (EmacsView): Disable focus
highlight.
(onCreateContextMenu, popupMenu, cancelPopupMenu): New
functions.
* java/org/gnu/emacs/EmacsWindow.java (EmacsWindow): Implement a
kind of ``override redirect'' window for tooltips.
* src/android.c (struct android_emacs_service): New method
`name_keysym'.
(android_run_select_thread, android_init_events):
(android_select): Release select thread on semaphores instead of
signals to avoid one nasty race on SIGUSR2 delivery.
(android_init_emacs_service): Initialize new method.
(android_create_window): Handle CW_OVERRIDE_REDIRECT.
(android_move_resize_window, android_map_raised)
(android_translate_coordinates, android_get_keysym_name)
(android_build_string, android_exception_check): New functions.
* src/android.h: Update prototypes.

* src/androidfns.c (android_set_parent_frame, Fx_create_frame)
(unwind_create_tip_frame, android_create_tip_frame)
(android_hide_tip, compute_tip_xy, Fx_show_tip, Fx_hide_tip)
(syms_of_androidfns): Implement tooltips and iconification
reporting.

* src/androidgui.h (enum android_window_value_mask): Add
CWOverrideRedirect.
(struct android_set_window_attributes): Add `override_redirect'.
(ANDROID_IS_MODIFIER_KEY): Recognize Caps Lock.

* src/androidmenu.c (struct android_emacs_context_menu): New
struct.
(android_init_emacs_context_menu, android_unwind_local_frame)
(android_push_local_frame, android_menu_show, init_androidmenu):
New functions.

* src/androidterm.c (handle_one_android_event): Fix NULL pointer
dereference.
(android_fullscreen_hook): Handle fullscreen correctly.
(android_draw_box_rect): Fix top line.
(get_keysym_name): Implement function.
(android_create_terminal): Remove scroll bar stubs and add menu
hook.

* src/androidterm.h: Update prototypes.
* src/emacs.c (android_emacs_init): Initialize androidmenu.c.
* xcompile/Makefile.in: Fix clean rules.
2023-01-14 22:12:16 +08:00

473 lines
11 KiB
Java

/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
Copyright (C) 2023 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 android.content.res.ColorStateList;
import android.view.ContextMenu;
import android.view.View;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.Paint;
import android.os.Build;
import android.util.Log;
/* This is an Android view which has a back and front buffer. When
swapBuffers is called, the back buffer is swapped to the front
buffer, and any damage is invalidated. frontBitmap and backBitmap
are modified and used both from the UI and the Emacs thread. As a
result, there is a lock held during all drawing operations.
It is also a ViewGroup, as it also lays out children. */
public class EmacsView extends ViewGroup
{
public static final String TAG = "EmacsView";
/* The associated EmacsWindow. */
public EmacsWindow window;
/* The buffer bitmap. */
public Bitmap bitmap;
/* The associated canvases. */
public Canvas canvas;
/* The damage region. */
public Region damageRegion;
/* The paint. */
public Paint paint;
/* The associated surface view. */
private EmacsSurfaceView surfaceView;
/* Whether or not a configure event must be sent for the next layout
event regardless of what changed. */
public boolean mustReportLayout;
/* If non-null, whether or not bitmaps must be recreated upon the
next call to getBitmap. */
private Rect bitmapDirty;
/* Whether or not a popup is active. */
private boolean popupActive;
/* The current context menu. */
private EmacsContextMenu contextMenu;
public
EmacsView (EmacsWindow window)
{
super (EmacsService.SERVICE);
this.window = window;
this.damageRegion = new Region ();
this.paint = new Paint ();
setFocusable (true);
setFocusableInTouchMode (true);
/* Create the surface view. */
this.surfaceView = new EmacsSurfaceView (this);
this.surfaceView.setZOrderMediaOverlay (true);
addView (this.surfaceView);
/* Not sure exactly what this does but it makes things magically
work. Why is something as simple as XRaiseWindow so involved
on Android? */
setChildrenDrawingOrderEnabled (true);
/* Get rid of the foreground and background tint. */
setBackgroundTintList (null);
setForegroundTintList (null);
/* Get rid of the default focus highlight. */
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O)
setDefaultFocusHighlightEnabled (false);
}
private void
handleDirtyBitmap ()
{
Bitmap oldBitmap;
/* Save the old bitmap. */
oldBitmap = bitmap;
/* Recreate the front and back buffer bitmaps. */
bitmap
= Bitmap.createBitmap (bitmapDirty.width (),
bitmapDirty.height (),
Bitmap.Config.ARGB_8888);
bitmap.eraseColor (0xffffffff);
/* And canvases. */
canvas = new Canvas (bitmap);
/* Copy over the contents of the old bitmap. */
if (oldBitmap != null)
canvas.drawBitmap (oldBitmap, 0f, 0f, new Paint ());
bitmapDirty = null;
}
public synchronized void
explicitlyDirtyBitmap (Rect rect)
{
if (bitmapDirty == null
&& (bitmap == null
|| rect.width () != bitmap.getWidth ()
|| rect.height () != bitmap.getHeight ()))
bitmapDirty = rect;
}
public synchronized Bitmap
getBitmap ()
{
if (bitmapDirty != null)
handleDirtyBitmap ();
return bitmap;
}
public synchronized Canvas
getCanvas ()
{
if (bitmapDirty != null)
handleDirtyBitmap ();
return canvas;
}
@Override
protected void
onMeasure (int widthMeasureSpec, int heightMeasureSpec)
{
Rect measurements;
int width, height;
/* Return the width and height of the window regardless of what
the parent says. */
measurements = window.getGeometry ();
width = measurements.width ();
height = measurements.height ();
/* Now apply any extra requirements in widthMeasureSpec and
heightMeasureSpec. */
if (MeasureSpec.getMode (widthMeasureSpec) == MeasureSpec.EXACTLY)
width = MeasureSpec.getSize (widthMeasureSpec);
else if (MeasureSpec.getMode (widthMeasureSpec) == MeasureSpec.AT_MOST
&& width > MeasureSpec.getSize (widthMeasureSpec))
width = MeasureSpec.getSize (widthMeasureSpec);
if (MeasureSpec.getMode (heightMeasureSpec) == MeasureSpec.EXACTLY)
height = MeasureSpec.getSize (heightMeasureSpec);
else if (MeasureSpec.getMode (heightMeasureSpec) == MeasureSpec.AT_MOST
&& height > MeasureSpec.getSize (heightMeasureSpec))
height = MeasureSpec.getSize (heightMeasureSpec);
super.setMeasuredDimension (width, height);
}
@Override
protected synchronized void
onLayout (boolean changed, int left, int top, int right,
int bottom)
{
int count, i;
View child;
Rect windowRect;
count = getChildCount ();
if (changed || mustReportLayout)
{
mustReportLayout = false;
window.viewLayout (left, top, right, bottom);
}
if (changed
/* Check that a change has really happened. */
&& (bitmapDirty == null
|| bitmapDirty.width () != right - left
|| bitmapDirty.height () != bottom - top))
bitmapDirty = new Rect (left, top, right, bottom);
for (i = 0; i < count; ++i)
{
child = getChildAt (i);
Log.d (TAG, "onLayout: " + child);
if (child == surfaceView)
/* The child is the surface view, so give it the entire
view. */
child.layout (0, 0, right - left, bottom - top);
else if (child.getVisibility () != GONE)
{
if (!(child instanceof EmacsView))
continue;
/* What to do: lay out the view precisely according to its
window rect. */
windowRect = ((EmacsView) child).window.getGeometry ();
child.layout (windowRect.left, windowRect.top,
windowRect.right, windowRect.bottom);
}
}
}
public void
damageRect (Rect damageRect)
{
synchronized (damageRegion)
{
damageRegion.union (damageRect);
}
}
/* This method is called from both the UI thread and the Emacs
thread. */
public void
swapBuffers (boolean force)
{
Canvas canvas;
Rect damageRect;
Bitmap bitmap;
/* Code must always take damageRegion, and then surfaceChangeLock,
never the other way around! */
synchronized (damageRegion)
{
if (damageRegion.isEmpty ())
return;
bitmap = getBitmap ();
/* Emacs must take the following lock to ensure the access to the
canvas occurs with the surface created. Otherwise, Android
will throttle calls to lockCanvas. */
synchronized (surfaceView.surfaceChangeLock)
{
damageRect = damageRegion.getBounds ();
if (!surfaceView.isCreated ())
return;
if (bitmap == null)
return;
/* Lock the canvas with the specified damage. */
canvas = surfaceView.lockCanvas (damageRect);
/* Return if locking the canvas failed. */
if (canvas == null)
return;
/* Copy from the back buffer to the canvas. If damageRect was
made empty, then draw the entire back buffer. */
if (damageRect.isEmpty ())
canvas.drawBitmap (bitmap, 0f, 0f, paint);
else
canvas.drawBitmap (bitmap, damageRect, damageRect, paint);
/* Unlock the canvas and clear the damage. */
surfaceView.unlockCanvasAndPost (canvas);
damageRegion.setEmpty ();
}
}
}
public void
swapBuffers ()
{
swapBuffers (false);
}
@Override
public boolean
onKeyDown (int keyCode, KeyEvent event)
{
window.onKeyDown (keyCode, event);
return true;
}
@Override
public boolean
onKeyMultiple (int keyCode, int repeatCount, KeyEvent event)
{
window.onKeyDown (keyCode, event);
return true;
}
@Override
public boolean
onKeyUp (int keyCode, KeyEvent event)
{
window.onKeyUp (keyCode, event);
return true;
}
@Override
public void
onFocusChanged (boolean gainFocus, int direction,
Rect previouslyFocusedRect)
{
window.onFocusChanged (gainFocus);
super.onFocusChanged (gainFocus, direction,
previouslyFocusedRect);
}
@Override
public boolean
onGenericMotionEvent (MotionEvent motion)
{
return window.onSomeKindOfMotionEvent (motion);
}
@Override
public boolean
onTouchEvent (MotionEvent motion)
{
return window.onTouchEvent (motion);
}
private void
moveChildToBack (View child)
{
int index;
index = indexOfChild (child);
if (index > 0)
{
detachViewFromParent (index);
/* The view at 0 is the surface view. */
attachViewToParent (child, 1,
child.getLayoutParams());
}
}
/* The following two functions must not be called if the view has no
parent, or is parented to an activity. */
public void
raise ()
{
EmacsView parent;
parent = (EmacsView) getParent ();
Log.d (TAG, "raise: parent " + parent);
if (parent.indexOfChild (this)
== parent.getChildCount () - 1)
return;
parent.bringChildToFront (this);
/* Yes, all of this is really necessary! */
parent.requestLayout ();
parent.invalidate ();
requestLayout ();
invalidate ();
/* The surface view must be destroyed and recreated. */
removeView (surfaceView);
addView (surfaceView, 0);
}
public void
lower ()
{
EmacsView parent;
parent = (EmacsView) getParent ();
Log.d (TAG, "lower: parent " + parent);
if (parent.indexOfChild (this) == 1)
return;
parent.moveChildToBack (this);
/* Yes, all of this is really necessary! */
parent.requestLayout ();
parent.invalidate ();
requestLayout ();
invalidate ();
/* The surface view must be removed and attached again. */
removeView (surfaceView);
addView (surfaceView, 0);
}
@Override
protected void
onCreateContextMenu (ContextMenu menu)
{
if (contextMenu == null)
return;
contextMenu.expandTo (menu);
}
public boolean
popupMenu (EmacsContextMenu menu, int xPosition,
int yPosition)
{
if (popupActive)
return false;
contextMenu = menu;
/* On API 21 or later, use showContextMenu (float, float). */
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP)
return showContextMenu ((float) xPosition, (float) yPosition);
else
return showContextMenu ();
}
public void
cancelPopupMenu ()
{
if (!popupActive)
throw new IllegalStateException ("cancelPopupMenu called without"
+ " popupActive set");
contextMenu = null;
}
};