diff options
Diffstat (limited to 'java/org/gnu/emacs/EmacsView.java')
| -rw-r--r-- | java/org/gnu/emacs/EmacsView.java | 777 |
1 files changed, 777 insertions, 0 deletions
diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java new file mode 100644 index 00000000000..12d8ff4da56 --- /dev/null +++ b/java/org/gnu/emacs/EmacsView.java @@ -0,0 +1,777 @@ +/* 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.Context; + +import android.text.InputType; + +import android.view.ContextMenu; +import android.view.View; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; + +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; + +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 final class EmacsView extends ViewGroup + implements ViewTreeObserver.OnGlobalLayoutListener +{ + 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 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; + + /* Whether or not bitmaps must be recreated upon the next call to + getBitmap. */ + private boolean bitmapDirty; + + /* Whether or not a popup is active. */ + private boolean popupActive; + + /* The current context menu. */ + private EmacsContextMenu contextMenu; + + /* The last measured width and height. */ + private int measuredWidth, measuredHeight; + + /* Object acting as a lock for those values. */ + private Object dimensionsLock; + + /* The serial of the last clip rectangle change. */ + private long lastClipSerial; + + /* The InputMethodManager for this view's context. */ + public InputMethodManager imManager; + + /* Whether or not this view is attached to a window. */ + public boolean isAttachedToWindow; + + /* Whether or not this view should have the on screen keyboard + displayed whenever possible. */ + public boolean isCurrentlyTextEditor; + + /* The associated input connection. */ + private EmacsInputConnection inputConnection; + + /* The current IC mode. See `android_reset_ic' for more + details. */ + private int icMode; + + /* The number of calls to `resetIC' to have taken place the last + time an InputConnection was created. */ + public long icSerial; + + /* The number of calls to `recetIC' that have taken place. */ + public volatile long icGeneration; + + public + EmacsView (EmacsWindow window) + { + super (EmacsService.SERVICE); + + Object tem; + Context context; + + this.window = window; + this.damageRegion = new Region (); + + setFocusable (true); + setFocusableInTouchMode (true); + + /* Create the surface view. */ + this.surfaceView = new EmacsSurfaceView (this); + addView (this.surfaceView); + + /* Get rid of the default focus highlight. */ + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) + setDefaultFocusHighlightEnabled (false); + + /* Obtain the input method manager. */ + context = getContext (); + tem = context.getSystemService (Context.INPUT_METHOD_SERVICE); + imManager = (InputMethodManager) tem; + + /* Add this view as its own global layout listener. */ + getViewTreeObserver ().addOnGlobalLayoutListener (this); + + /* Create an object used as a lock. */ + this.dimensionsLock = new Object (); + } + + private void + handleDirtyBitmap () + { + Bitmap oldBitmap; + int measuredWidth, measuredHeight; + + synchronized (dimensionsLock) + { + /* Load measuredWidth and measuredHeight. */ + measuredWidth = this.measuredWidth; + measuredHeight = this.measuredHeight; + } + + if (measuredWidth == 0 || measuredHeight == 0) + return; + + if (!isAttachedToWindow) + return; + + /* If bitmap is the same width and height as the measured width + and height, there is no need to do anything. Avoid allocating + the extra bitmap. */ + if (bitmap != null + && (bitmap.getWidth () == measuredWidth + && bitmap.getHeight () == measuredHeight)) + { + bitmapDirty = false; + return; + } + + /* Save the old bitmap. */ + oldBitmap = bitmap; + + /* Recreate the back buffer bitmap. */ + bitmap + = Bitmap.createBitmap (measuredWidth, + measuredHeight, + Bitmap.Config.ARGB_8888); + bitmap.eraseColor (window.background | 0xff000000); + + /* And canvases. */ + canvas = new Canvas (bitmap); + canvas.save (); + + /* Since the clip rectangles have been cleared, clear the clip + rectangle ID. */ + lastClipSerial = 0; + + /* Copy over the contents of the old bitmap. */ + if (oldBitmap != null) + canvas.drawBitmap (oldBitmap, 0f, 0f, new Paint ()); + + bitmapDirty = false; + + /* Explicitly free the old bitmap's memory. */ + + if (oldBitmap != null) + oldBitmap.recycle (); + + /* Some Android versions still don't free the bitmap until the + next GC. */ + Runtime.getRuntime ().gc (); + } + + public synchronized void + explicitlyDirtyBitmap () + { + bitmapDirty = true; + } + + public synchronized Bitmap + getBitmap () + { + if (bitmapDirty || bitmap == null) + handleDirtyBitmap (); + + return bitmap; + } + + public synchronized Canvas + getCanvas (EmacsGC gc) + { + int i; + + if (bitmapDirty || bitmap == null) + handleDirtyBitmap (); + + if (canvas == null) + return null; + + /* Update clip rectangles if necessary. */ + if (gc.clipRectID != lastClipSerial) + { + canvas.restore (); + canvas.save (); + + if (gc.real_clip_rects != null) + { + for (i = 0; i < gc.real_clip_rects.length; ++i) + canvas.clipRect (gc.real_clip_rects[i]); + } + + lastClipSerial = gc.clipRectID; + } + + return canvas; + } + + public void + prepareForLayout (int wantedWidth, int wantedHeight) + { + synchronized (dimensionsLock) + { + measuredWidth = wantedWidth; + measuredHeight = wantedWidth; + } + } + + @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); + } + + /* Note that the monitor lock for the window must never be held from + within the lock for the view, because the window also locks the + other way around. */ + + @Override + protected void + onLayout (boolean changed, int left, int top, int right, + int bottom) + { + int count, i, oldMeasuredWidth, oldMeasuredHeight; + View child; + Rect windowRect; + boolean needExpose; + + count = getChildCount (); + needExpose = false; + + synchronized (dimensionsLock) + { + /* Load measuredWidth and measuredHeight. */ + oldMeasuredWidth = measuredWidth; + oldMeasuredHeight = measuredHeight; + + /* Set measuredWidth and measuredHeight. */ + measuredWidth = right - left; + measuredHeight = bottom - top; + } + + /* Dirty the back buffer if the layout change resulted in the view + being resized. */ + + if (changed && (right - left != oldMeasuredWidth + || bottom - top != oldMeasuredHeight)) + { + explicitlyDirtyBitmap (); + + /* Expose the window upon a change in the view's size. */ + + if (right - left > oldMeasuredWidth + || bottom - top > oldMeasuredHeight) + needExpose = true; + } + + for (i = 0; i < count; ++i) + { + child = getChildAt (i); + + Log.d (TAG, "onLayout: " + child); + + if (child == surfaceView) + 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); + } + } + + /* Now report the layout change to the window. */ + + if (changed || mustReportLayout) + { + mustReportLayout = false; + window.viewLayout (left, top, right, bottom); + } + + if (needExpose) + EmacsNative.sendExpose (this.window.handle, 0, 0, + right - left, bottom - top); + } + + public void + damageRect (Rect damageRect) + { + EmacsService.checkEmacsThread (); + damageRegion.union (damageRect); + } + + /* This method is called from both the UI thread and the Emacs + thread. */ + + public void + swapBuffers () + { + Canvas canvas; + Rect damageRect; + Bitmap bitmap; + + /* Make sure this function is called only from the Emacs + thread. */ + EmacsService.checkEmacsThread (); + + damageRect = null; + + /* Now see if there is a damage region. */ + + if (damageRegion.isEmpty ()) + return; + + /* And extract and clear the damage region. */ + + damageRect = damageRegion.getBounds (); + damageRegion.setEmpty (); + + bitmap = getBitmap (); + + /* Transfer the bitmap to the surface view, then invalidate + it. */ + surfaceView.setBitmap (bitmap, damageRect); + } + + @Override + public boolean + onKeyDown (int keyCode, KeyEvent event) + { + if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP + || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN + || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) + && !EmacsNative.shouldForwardMultimediaButtons ()) + return false; + + window.onKeyDown (keyCode, event); + return true; + } + + @Override + public boolean + onKeyMultiple (int keyCode, int repeatCount, KeyEvent event) + { + if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP + || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN + || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) + && !EmacsNative.shouldForwardMultimediaButtons ()) + return false; + + window.onKeyDown (keyCode, event); + return true; + } + + @Override + public boolean + onKeyUp (int keyCode, KeyEvent event) + { + if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP + || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN + || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) + && !EmacsNative.shouldForwardMultimediaButtons ()) + return false; + + 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.onGenericMotionEvent (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); + } + + public void + lower () + { + EmacsView parent; + + parent = (EmacsView) getParent (); + + Log.d (TAG, "lower: parent " + parent); + + if (parent.indexOfChild (this) == 1) + return; + + parent.moveChildToBack (this); + } + + @Override + protected void + onCreateContextMenu (ContextMenu menu) + { + if (contextMenu == null) + return; + + contextMenu.expandTo (menu, this); + } + + public boolean + popupMenu (EmacsContextMenu menu, int xPosition, + int yPosition, boolean force) + { + if (popupActive && !force) + return false; + + contextMenu = menu; + popupActive = true; + + Log.d (TAG, "popupMenu: " + menu + " @" + xPosition + + ", " + yPosition + " " + force); + + /* Use showContextMenu (float, float) on N to get actual popup + behavior. */ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + return showContextMenu ((float) xPosition, (float) yPosition); + else + return showContextMenu (); + } + + public void + cancelPopupMenu () + { + if (!popupActive) + throw new IllegalStateException ("cancelPopupMenu called without" + + " popupActive set"); + + contextMenu = null; + popupActive = false; + + /* It is not possible to know with 100% certainty which activity + is currently displaying the context menu. Loop through each + activity and call `closeContextMenu' instead. */ + + for (EmacsWindowAttachmentManager.WindowConsumer consumer + : EmacsWindowAttachmentManager.MANAGER.consumers) + { + if (consumer instanceof EmacsActivity) + ((EmacsActivity) consumer).closeContextMenu (); + } + } + + @Override + public synchronized void + onDetachedFromWindow () + { + isAttachedToWindow = false; + + /* Recycle the bitmap and call GC. */ + + if (bitmap != null) + bitmap.recycle (); + + bitmap = null; + canvas = null; + surfaceView.setBitmap (null, null); + + /* Collect the bitmap storage; it could be large. */ + Runtime.getRuntime ().gc (); + + super.onDetachedFromWindow (); + } + + @Override + public synchronized void + onAttachedToWindow () + { + isAttachedToWindow = true; + + /* Dirty the bitmap, as it was destroyed when onDetachedFromWindow + was called. */ + bitmapDirty = true; + + synchronized (dimensionsLock) + { + /* Now expose the view contents again. */ + EmacsNative.sendExpose (this.window.handle, 0, 0, + measuredWidth, measuredHeight); + } + + super.onAttachedToWindow (); + } + + public void + showOnScreenKeyboard () + { + /* Specifying no flags at all tells the system the user asked for + the input method to be displayed. */ + + imManager.showSoftInput (this, 0); + isCurrentlyTextEditor = true; + } + + public void + hideOnScreenKeyboard () + { + imManager.hideSoftInputFromWindow (this.getWindowToken (), + 0); + isCurrentlyTextEditor = false; + } + + @Override + public InputConnection + onCreateInputConnection (EditorInfo info) + { + int mode; + int[] selection; + + /* Figure out what kind of IME behavior Emacs wants. */ + mode = getICMode (); + + /* Make sure the input method never displays a full screen input + box that obscures Emacs. */ + info.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN; + info.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI; + + /* Set a reasonable inputType. */ + info.inputType = InputType.TYPE_CLASS_TEXT; + + /* If this fails or ANDROID_IC_MODE_NULL was requested, then don't + initialize the input connection. */ + + if (mode == EmacsService.IC_MODE_NULL) + { + info.inputType = InputType.TYPE_NULL; + return null; + } + + /* Set icSerial. If icSerial < icGeneration, the input connection + has been reset, and future input should be ignored until a new + connection is created. */ + + icSerial = icGeneration; + + /* Reset flags set by the previous input method. */ + + EmacsNative.clearInputFlags (window.handle); + + /* Obtain the current position of point and set it as the + selection. Don't do this under one specific situation: if + `android_update_ic' is being called in the main thread, trying + to synchronize with it can cause a dead lock in the IM manager. + See icBeginSynchronous in EmacsService.java for more + details. */ + + selection = EmacsService.viewGetSelection (window.handle); + + if (selection != null) + Log.d (TAG, "onCreateInputConnection: current selection is: " + + selection[0] + ", by " + selection[1]); + else + { + Log.d (TAG, "onCreateInputConnection: current selection could" + + " not be retrieved."); + + /* If the selection could not be obtained, return 0 by 0. + However, ask for the selection position to be updated as + soon as possible. */ + + selection = new int[] { 0, 0, }; + EmacsNative.requestSelectionUpdate (window.handle); + } + + if (mode == EmacsService.IC_MODE_ACTION) + info.imeOptions |= EditorInfo.IME_ACTION_DONE; + + /* Set the initial selection fields. */ + info.initialSelStart = selection[0]; + info.initialSelEnd = selection[1]; + + /* Create the input connection if necessary. */ + + if (inputConnection == null) + inputConnection = new EmacsInputConnection (this); + else + /* Clear several pieces of state in the input connection. */ + inputConnection.reset (); + + /* Return the input connection. */ + return inputConnection; + } + + @Override + public synchronized boolean + onCheckIsTextEditor () + { + /* If value is true, then the system will display the on screen + keyboard. */ + return isCurrentlyTextEditor; + } + + @Override + public boolean + isOpaque () + { + /* Returning true here allows the system to not draw the contents + of windows underneath this view, thereby improving + performance. */ + return true; + } + + public synchronized void + setICMode (int icMode) + { + this.icMode = icMode; + } + + public synchronized int + getICMode () + { + return icMode; + } + + @Override + public void + onGlobalLayout () + { + int[] locations; + + /* Get the absolute offset of this view and specify its left and + top position in subsequent ConfigureNotify events. */ + + locations = new int[2]; + getLocationInWindow (locations); + window.notifyContentRectPosition (locations[0], + locations[1]); + } +}; |
