diff options
Diffstat (limited to 'android/source/src/java/org/libreoffice/overlay')
4 files changed, 1382 insertions, 0 deletions
diff --git a/android/source/src/java/org/libreoffice/overlay/CalcHeadersController.java b/android/source/src/java/org/libreoffice/overlay/CalcHeadersController.java new file mode 100644 index 000000000..8b99c292c --- /dev/null +++ b/android/source/src/java/org/libreoffice/overlay/CalcHeadersController.java @@ -0,0 +1,281 @@ +package org.libreoffice.overlay; + +import android.content.Context; +import android.graphics.PointF; +import android.graphics.RectF; +import android.graphics.drawable.ColorDrawable; +import com.google.android.material.snackbar.Snackbar; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup.LayoutParams; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; +import android.widget.Button; +import android.widget.PopupWindow; +import android.widget.TextView; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.libreoffice.LOEvent; +import org.libreoffice.LOKitShell; +import org.libreoffice.LibreOfficeMainActivity; +import org.libreoffice.R; +import org.mozilla.gecko.gfx.LayerView; + +import java.math.BigDecimal; +import java.util.ArrayList; + +import static org.libreoffice.SearchController.addProperty; + +public class CalcHeadersController { + private static final String LOGTAG = CalcHeadersController.class.getSimpleName(); + + private final CalcHeadersView mCalcRowHeadersView; + private final CalcHeadersView mCalcColumnHeadersView; + + private LibreOfficeMainActivity mContext; + + public CalcHeadersController(LibreOfficeMainActivity context, final LayerView layerView) { + mContext = context; + mContext.getDocumentOverlay().setCalcHeadersController(this); + mCalcRowHeadersView = context.findViewById(R.id.calc_header_row); + mCalcColumnHeadersView = context.findViewById(R.id.calc_header_column); + if (mCalcColumnHeadersView == null || mCalcRowHeadersView == null) { + Log.e(LOGTAG, "Failed to initialize Calc headers - View is null"); + } else { + mCalcRowHeadersView.initialize(layerView, true); + mCalcColumnHeadersView.initialize(layerView, false); + } + LOKitShell.sendEvent(new LOEvent(LOEvent.UPDATE_CALC_HEADERS)); + context.findViewById(R.id.calc_header_top_left).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SelectAll")); + if (mCalcColumnHeadersView == null) return; + mCalcColumnHeadersView.showHeaderPopup(new PointF()); + } + }); + ((EditText)context.findViewById(R.id.calc_address)).setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_ACTION_GO) { + String text = v.getText().toString(); + JSONObject rootJson = new JSONObject(); + try { + addProperty(rootJson, "ToPoint", "string", text); + } catch (JSONException e) { + e.printStackTrace(); + } + LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:GoToCell", rootJson.toString())); + mContext.hideSoftKeyboard(); + layerView.requestFocus(); + } + return true; + } + }); + ((EditText)context.findViewById(R.id.calc_formula)).setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_ACTION_GO) { + String text = v.getText().toString(); + JSONObject rootJson = new JSONObject(); + try { + addProperty(rootJson, "StringName", "string", text); + addProperty(rootJson, "DontCommit", "boolean", String.valueOf(false)); + } catch (JSONException e) { + e.printStackTrace(); + } + LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:EnterString", rootJson.toString())); + mContext.hideSoftKeyboard(); + layerView.requestFocus(); + mContext.setDocumentChanged(true); + } + return true; + } + }); + // manually select A1 for address bar and formula bar to update when calc first opens + JSONObject rootJson = new JSONObject(); + try { + addProperty(rootJson, "ToPoint", "string", "A1"); + } catch (JSONException e) { + e.printStackTrace(); + } + LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:GoToCell", rootJson.toString())); + } + + public void setupHeaderPopupView() { + LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + String[] rowOrColumn = {"Row","Column"}; + CalcHeadersView[] headersViews= {mCalcRowHeadersView, mCalcColumnHeadersView}; + for (int i = 0; i < rowOrColumn.length; i++) { + // create popup window + final String tempName = rowOrColumn[i]; + final CalcHeadersView tempView = headersViews[i]; + final View headerPopupView = inflater.inflate(R.layout.calc_header_popup, null); + final PopupWindow popupWindow = new PopupWindow(headerPopupView, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + popupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() { + @Override + public void onDismiss() { + headerPopupView.findViewById(R.id.calc_header_popup_optimal_length_dialog).setVisibility(View.GONE); + popupWindow.setFocusable(false); + } + }); + popupWindow.setOutsideTouchable(true); + popupWindow.setBackgroundDrawable(new ColorDrawable()); + popupWindow.setAnimationStyle(android.R.style.Animation_Dialog); + tempView.setHeaderPopupWindow(popupWindow); + // set up child views in the popup window + headerPopupView.findViewById(R.id.calc_header_popup_insert).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Insert"+tempName+"s")); + tempView.dismissPopupWindow(); + mContext.setDocumentChanged(true); + } + }); + headerPopupView.findViewById(R.id.calc_header_popup_delete).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Delete"+tempName+"s")); + tempView.dismissPopupWindow(); + mContext.setDocumentChanged(true); + } + }); + headerPopupView.findViewById(R.id.calc_header_popup_hide).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Hide"+tempName)); + tempView.dismissPopupWindow(); + mContext.setDocumentChanged(true); + } + }); + headerPopupView.findViewById(R.id.calc_header_popup_show).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:Show"+tempName)); + tempView.dismissPopupWindow(); + mContext.setDocumentChanged(true); + } + }); + headerPopupView.findViewById(R.id.calc_header_popup_optimal_length).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + View view = headerPopupView.findViewById(R.id.calc_header_popup_optimal_length_dialog); + if (view.getVisibility() == View.VISIBLE) { + view.setVisibility(View.GONE); + popupWindow.setFocusable(false); + popupWindow.update(); + } else { + popupWindow.dismiss(); + view.setVisibility(View.VISIBLE); + popupWindow.setFocusable(true); + popupWindow.showAtLocation(tempView, Gravity.CENTER, 0, 0); + LOKitShell.getMainHandler().post(new Runnable() { + @Override + public void run() { + Snackbar.make(tempView, R.string.calc_alert_double_click_optimal_length, Snackbar.LENGTH_LONG).show(); + } + }); + } + } + }); + headerPopupView.findViewById(R.id.calc_header_popup_optimal_length_button).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String text = ((EditText)headerPopupView.findViewById(R.id.calc_header_popup_optimal_length_text)).getText().toString(); + tempView.sendOptimalLengthRequest(text); + tempView.dismissPopupWindow(); + mContext.setDocumentChanged(true); + } + }); + headerPopupView.findViewById(R.id.calc_header_popup_adjust_length).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mContext.getDocumentOverlay().showAdjustLengthLine(tempView == mCalcRowHeadersView, tempView); + tempView.dismissPopupWindow(); + mContext.setDocumentChanged(true); + } + }); + ((Button)headerPopupView.findViewById(R.id.calc_header_popup_adjust_length)) + .setText(tempView == mCalcRowHeadersView ? R.string.calc_adjust_height : R.string.calc_adjust_width); + ((Button)headerPopupView.findViewById(R.id.calc_header_popup_optimal_length)) + .setText(tempView == mCalcRowHeadersView ? R.string.calc_optimal_height : R.string.calc_optimal_width); + + } + } + + public void setHeaders(String headers) { + HeaderInfo parsedHeaders = parseHeaderInfo(headers); + if (parsedHeaders != null) { + mCalcRowHeadersView.setHeaders(parsedHeaders.rowLabels, parsedHeaders.rowDimens); + mCalcColumnHeadersView.setHeaders(parsedHeaders.columnLabels, parsedHeaders.columnDimens); + showHeaders(); + } else { + Log.e(LOGTAG, "Parse header info JSON failed."); + } + } + + public void showHeaders() { + LOKitShell.getMainHandler().post(new Runnable() { + @Override + public void run() { + mCalcColumnHeadersView.invalidate(); + mCalcRowHeadersView.invalidate(); + } + }); + } + + private HeaderInfo parseHeaderInfo(String headers) { + HeaderInfo headerInfo = new HeaderInfo(); + try { + JSONObject collectiveResult = new JSONObject(headers); + JSONArray rowResult = collectiveResult.getJSONArray("rows"); + for (int i = 0; i < rowResult.length(); i++) { + headerInfo.rowLabels.add(rowResult.getJSONObject(i).getString("text")); + headerInfo.rowDimens.add(BigDecimal.valueOf(rowResult.getJSONObject(i).getLong("size")).floatValue()); + } + JSONArray columnResult = collectiveResult.getJSONArray("columns"); + for (int i = 0; i < columnResult.length(); i++) { + headerInfo.columnLabels.add(columnResult.getJSONObject(i).getString("text")); + headerInfo.columnDimens.add(BigDecimal.valueOf(columnResult.getJSONObject(i).getLong("size")).floatValue()); + } + return headerInfo; + } catch (JSONException e) { + e.printStackTrace(); + } + return null; + } + + public void showHeaderSelection(RectF cellCursorRect) { + mCalcRowHeadersView.setHeaderSelection(cellCursorRect); + mCalcColumnHeadersView.setHeaderSelection(cellCursorRect); + showHeaders(); + } + + public void setPendingRowOrColumnSelectionToShowUp(boolean b) { + mCalcRowHeadersView.setPendingRowOrColumnSelectionToShowUp(b); + mCalcColumnHeadersView.setPendingRowOrColumnSelectionToShowUp(b); + } + + public boolean pendingRowOrColumnSelectionToShowUp() { + return mCalcColumnHeadersView.pendingRowOrColumnSelectionToShowUp() + || mCalcRowHeadersView.pendingRowOrColumnSelectionToShowUp(); + } + + private class HeaderInfo { + ArrayList<String> rowLabels; + ArrayList<Float> rowDimens; + ArrayList<String> columnLabels; + ArrayList<Float> columnDimens; + private HeaderInfo() { + rowLabels = new ArrayList<String>(); + rowDimens = new ArrayList<Float>(); + columnDimens = new ArrayList<Float>(); + columnLabels = new ArrayList<String>(); + } + } +} diff --git a/android/source/src/java/org/libreoffice/overlay/CalcHeadersView.java b/android/source/src/java/org/libreoffice/overlay/CalcHeadersView.java new file mode 100644 index 000000000..98af7a955 --- /dev/null +++ b/android/source/src/java/org/libreoffice/overlay/CalcHeadersView.java @@ -0,0 +1,278 @@ +package org.libreoffice.overlay; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.PointF; +import android.graphics.RectF; +import androidx.core.view.GestureDetectorCompat; +import android.util.AttributeSet; +import android.view.GestureDetector.SimpleOnGestureListener; +import android.view.MotionEvent; +import android.view.View; +import android.widget.PopupWindow; + +import org.json.JSONException; +import org.json.JSONObject; +import org.libreoffice.LOEvent; +import org.libreoffice.LOKitShell; +import org.libreoffice.LibreOfficeMainActivity; +import org.libreoffice.canvas.CalcHeaderCell; +import org.libreoffice.kit.Document; +import org.mozilla.gecko.gfx.ImmutableViewportMetrics; +import org.mozilla.gecko.gfx.LayerView; + +import java.util.ArrayList; +import java.util.Collections; + +import static org.libreoffice.SearchController.addProperty; + +public class CalcHeadersView extends View { + private static final String LOGTAG = CalcHeadersView.class.getSimpleName(); + + private boolean mInitialized; + private LayerView mLayerView; + private boolean mIsRow; // true if this is for row headers, false for column + private ArrayList<String> mLabels; + private ArrayList<Float> mDimens; + private RectF mCellCursorRect; + private boolean mPendingRowOrColumnSelectionToShowUp; + private GestureDetectorCompat mDetector; + private PopupWindow mPopupWindow; + private int mPrevScrollIndex = -1; + + public CalcHeadersView(Context context) { + super(context); + } + + public CalcHeadersView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CalcHeadersView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void initialize(LayerView layerView, boolean isRow) { + if (!mInitialized) { + mLayerView = layerView; + mIsRow = isRow; + + LOKitShell.getMainHandler().post(new Runnable() { + @Override + public void run() { + mDetector = new GestureDetectorCompat(getContext(), new HeaderGestureListener()); + } + }); + + setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getActionMasked() == MotionEvent.ACTION_UP) { + mPrevScrollIndex = -1; // clear mPrevScrollIndex to default + } + return mDetector.onTouchEvent(event); + } + }); + + mInitialized = true; + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (mInitialized && mDimens != null && mLabels != null) { + updateHeaders(canvas); + } + } + + private void updateHeaders(Canvas canvas) { + ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics(); + float zoom = metrics.getZoomFactor(); + PointF origin = metrics.getOrigin(); + + // Draw headers + boolean inRangeOfVisibleHeaders = false; // a helper variable for skipping unnecessary onDraw()'s + float top,bottom,left,right; + for (int i = 1; i < mLabels.size(); i++) { + if (mDimens.get(i).equals(mDimens.get(i-1))) continue; + if (mIsRow) { + top = -origin.y + zoom*mDimens.get(i-1); + bottom = -origin.y + zoom*mDimens.get(i); + if (top <= getHeight() && bottom >= 0) { + inRangeOfVisibleHeaders = true; + boolean isSelected = mCellCursorRect != null && bottom > mCellCursorRect.top - origin.y && top < mCellCursorRect.bottom - origin.y; + new CalcHeaderCell(0f, top, getWidth(), bottom - top, mLabels.get(i), isSelected).onDraw(canvas); + } else { + if (inRangeOfVisibleHeaders) { + break; + } + } + } else { + left = -origin.x + zoom*mDimens.get(i-1); + right = -origin.x + zoom*mDimens.get(i); + if (left <= getWidth() && right >= 0) { + boolean isSelected = mCellCursorRect != null && right > mCellCursorRect.left - origin.x && left < mCellCursorRect.right - origin.x; + new CalcHeaderCell(left, 0f, right - left, getHeight(), mLabels.get(i), isSelected).onDraw(canvas); + } else { + if (inRangeOfVisibleHeaders) { + break; + } + } + } + } + } + + /** + * Handle a single tap event on a header cell. + * Selects whole row/column. + */ + private void highlightRowOrColumn(PointF point, boolean shift) { + int index = getIndexFromPointOfTouch(point); + try { + JSONObject rootJson = new JSONObject(); + if (shift) { + addProperty(rootJson, "Modifier", "unsigned short", + String.valueOf(Document.KEYBOARD_MODIFIER_SHIFT)); + } else { + addProperty(rootJson, "Modifier", "unsigned short", "0"); + } + if (mIsRow) { + addProperty(rootJson, "Row", "unsigned short", String.valueOf(index)); + LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SelectRow", rootJson.toString())); + } else { + addProperty(rootJson, "Col", "unsigned short", String.valueOf(index)); + LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SelectColumn", rootJson.toString())); + } + } catch (JSONException e) { + e.printStackTrace(); + } + // At this point, InvalidationHandler.java will have received two callbacks. + // One is for text selection (first) and the other for cell selection (second). + // The second will override the first on headers which is not wanted. + // setPendingRowOrColumnSelectionToShowUp(true) will skip the second call. + setPendingRowOrColumnSelectionToShowUp(true); + } + + public int getIndexFromPointOfTouch(PointF point) { + int searchedIndex, index; + ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics(); + float zoom = metrics.getZoomFactor(); + PointF origin = metrics.getOrigin(); + if (mIsRow) { + searchedIndex = Collections.binarySearch(mDimens, (point.y+origin.y)/zoom); + } else { + searchedIndex = Collections.binarySearch(mDimens, (point.x+origin.x)/zoom); + } + // converting searched index to real index on headers + if (searchedIndex < 0) { + index = - searchedIndex - 2; + } else { + index = searchedIndex; + } + return index; + } + + public void setPendingRowOrColumnSelectionToShowUp(boolean b) { + mPendingRowOrColumnSelectionToShowUp = b; + } + + public boolean pendingRowOrColumnSelectionToShowUp() { + return mPendingRowOrColumnSelectionToShowUp; + } + + public void setHeaders(ArrayList<String> labels, ArrayList<Float> dimens) { + mLabels = labels; + mDimens = dimens; + } + + public void setHeaderSelection(RectF cellCursorRect) { + mCellCursorRect = cellCursorRect; + } + + public void showHeaderPopup(PointF point) { + if (mPopupWindow == null || + !LibreOfficeMainActivity.isExperimentalMode()) return; + if (mIsRow) { + mPopupWindow.showAsDropDown(this, getWidth()*3/2, -getHeight()+(int)point.y); + } else { + mPopupWindow.showAsDropDown(this, (int)point.x, getHeight()/2); + } + } + + public void dismissPopupWindow() { + if (mPopupWindow == null) return; + mPopupWindow.dismiss(); + } + + public void setHeaderPopupWindow(PopupWindow popupWindow) { + if (mPopupWindow != null) return; + mPopupWindow = popupWindow; + } + + public void sendOptimalLengthRequest(String text) { + JSONObject rootJson = new JSONObject(); + if (mIsRow) { + try { + addProperty(rootJson, "aExtraHeight", "unsigned short", text); + LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SetOptimalRowHeight", rootJson.toString())); + } catch (JSONException ex) { + ex.printStackTrace(); + } + } else { + try { + addProperty(rootJson, "aExtraWidth", "unsigned short", text); + LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND, ".uno:SetOptimalColumnWidth", rootJson.toString())); + } catch (JSONException ex) { + ex.printStackTrace(); + } + } + } + + private class HeaderGestureListener extends SimpleOnGestureListener { + + @Override + public boolean onDown(MotionEvent e) { + return true; + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + PointF pointOfTouch = new PointF(e.getX(), e.getY()); + highlightRowOrColumn(pointOfTouch, false); + showHeaderPopup(pointOfTouch); + return true; + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + PointF point2 = new PointF(e2.getX(), e2.getY()); + if (mPrevScrollIndex != getIndexFromPointOfTouch(point2)) { + mPrevScrollIndex = getIndexFromPointOfTouch(point2); + highlightRowOrColumn(point2, true); + dismissPopupWindow(); + showHeaderPopup(point2); + } + return true; + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + PointF pointOfTouch = new PointF(e.getX(), e.getY()); + highlightRowOrColumn(pointOfTouch, false); + if (mIsRow) { + JSONObject rootJson = new JSONObject(); + try { + addProperty(rootJson, "aExtraHeight", "unsigned short", String.valueOf(0)); + } catch (JSONException ex) { + ex.printStackTrace(); + } + LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND,".uno:SetOptimalRowHeight", rootJson.toString())); + } else { + LOKitShell.sendEvent(new LOEvent(LOEvent.UNO_COMMAND,".uno:SetOptimalColumnWidthDirect")); + } + showHeaderPopup(pointOfTouch); + return true; + } + } +} diff --git a/android/source/src/java/org/libreoffice/overlay/DocumentOverlay.java b/android/source/src/java/org/libreoffice/overlay/DocumentOverlay.java new file mode 100644 index 000000000..f977866a2 --- /dev/null +++ b/android/source/src/java/org/libreoffice/overlay/DocumentOverlay.java @@ -0,0 +1,271 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.libreoffice.overlay; + +import android.graphics.RectF; +import android.util.Log; + +import org.libreoffice.LOKitShell; +import org.libreoffice.LibreOfficeMainActivity; +import org.libreoffice.R; +import org.libreoffice.canvas.SelectionHandle; +import org.mozilla.gecko.gfx.Layer; +import org.mozilla.gecko.gfx.LayerView; +import org.mozilla.gecko.util.FloatUtils; + +import java.util.List; + +/** + * The DocumentOverlay is an overlay over the document. This class is responsible + * to setup the document overlay view, report visibility and position of its elements + * when they change and report any changes to the viewport. + */ +public class DocumentOverlay { + private static final String LOGTAG = DocumentOverlay.class.getSimpleName(); + + private final DocumentOverlayView mDocumentOverlayView; + private final DocumentOverlayLayer mDocumentOverlayLayer; + + private final long hidePageNumberRectDelayInMilliseconds = 500; + + /** + * DocumentOverlayLayer responsibility is to get the changes to the viewport + * and report them to DocumentOverlayView. + */ + private class DocumentOverlayLayer extends Layer { + private float mViewLeft; + private float mViewTop; + private float mViewZoom; + + /** + * @see Layer#draw(org.mozilla.gecko.gfx.Layer.RenderContext) + */ + @Override + public void draw(final RenderContext context) { + if (FloatUtils.fuzzyEquals(mViewLeft, context.viewport.left) + && FloatUtils.fuzzyEquals(mViewTop, context.viewport.top) + && FloatUtils.fuzzyEquals(mViewZoom, context.zoomFactor)) { + return; + } + + mViewLeft = context.viewport.left; + mViewTop = context.viewport.top; + mViewZoom = context.zoomFactor; + + LOKitShell.getMainHandler().post(new Runnable() { + public void run() { + mDocumentOverlayView.repositionWithViewport(mViewLeft, mViewTop, mViewZoom); + } + }); + } + } + + public DocumentOverlay(LibreOfficeMainActivity context, LayerView layerView) { + mDocumentOverlayView = context.findViewById(R.id.text_cursor_view); + mDocumentOverlayLayer = new DocumentOverlayLayer(); + if (mDocumentOverlayView == null) { + Log.e(LOGTAG, "Failed to initialize TextCursorLayer - CursorView is null"); + } + layerView.addLayer(mDocumentOverlayLayer); + mDocumentOverlayView.initialize(layerView); + } + + public void setPartPageRectangles(List<RectF> rectangles) { + mDocumentOverlayView.setPartPageRectangles(rectangles); + } + + /** + * Show the cursor at the defined cursor position on the overlay. + */ + public void showCursor() { + LOKitShell.getMainHandler().post(new Runnable() { + public void run() { + mDocumentOverlayView.showCursor(); + } + }); + } + + /** + * Hide the cursor at the defined cursor position on the overlay. + */ + public void hideCursor() { + LOKitShell.getMainHandler().post(new Runnable() { + public void run() { + mDocumentOverlayView.hideCursor(); + } + }); + } + + /** + * Show the page number rectangle on the overlay. + */ + public void showPageNumberRect() { + LOKitShell.getMainHandler().post(new Runnable() { + public void run() { + mDocumentOverlayView.showPageNumberRect(); + } + }); + } + + /** + * Hide the page number rectangle on the overlay. + */ + public void hidePageNumberRect() { + LOKitShell.getMainHandler().postDelayed(new Runnable() { + public void run() { + mDocumentOverlayView.hidePageNumberRect(); + } + }, hidePageNumberRectDelayInMilliseconds); + } + + /** + * Position the cursor to the input position on the overlay. + */ + public void positionCursor(final RectF position) { + LOKitShell.getMainHandler().post(new Runnable() { + public void run() { + mDocumentOverlayView.changeCursorPosition(position); + } + }); + } + + /** + * Show selections on the overlay. + */ + public void showSelections() { + LOKitShell.getMainHandler().post(new Runnable() { + public void run() { + mDocumentOverlayView.showSelections(); + } + }); + } + + /** + * Hide selections on the overlay. + */ + public void hideSelections() { + LOKitShell.getMainHandler().post(new Runnable() { + public void run() { + mDocumentOverlayView.hideSelections(); + } + }); + } + + /** + * Change the list of selections. + */ + public void changeSelections(final List<RectF> selections) { + LOKitShell.getMainHandler().post(new Runnable() { + public void run() { + mDocumentOverlayView.changeSelections(selections); + } + }); + } + + /** + * Show the graphic selection on the overlay. + */ + public void showGraphicSelection() { + LOKitShell.getMainHandler().post(new Runnable() { + public void run() { + mDocumentOverlayView.showGraphicSelection(); + } + }); + } + + /** + * Hide the graphic selection. + */ + public void hideGraphicSelection() { + LOKitShell.getMainHandler().post(new Runnable() { + public void run() { + mDocumentOverlayView.hideGraphicSelection(); + } + }); + } + + /** + * Change the graphic selection rectangle to the input rectangle. + */ + public void changeGraphicSelection(final RectF rectangle) { + LOKitShell.getMainHandler().post(new Runnable() { + public void run() { + mDocumentOverlayView.changeGraphicSelection(rectangle); + } + }); + } + + /** + * Show the handle (of input type) on the overlay. + */ + public void showHandle(final SelectionHandle.HandleType type) { + LOKitShell.getMainHandler().post(new Runnable() { + public void run() { + mDocumentOverlayView.showHandle(type); + } + }); + } + + /** + * Hide the handle (of input type). + */ + public void hideHandle(final SelectionHandle.HandleType type) { + LOKitShell.getMainHandler().post(new Runnable() { + public void run() { + mDocumentOverlayView.hideHandle(type); + } + }); + } + + /** + * Position the handle (of input type) position to the input rectangle. + */ + public void positionHandle(final SelectionHandle.HandleType type, final RectF rectangle) { + LOKitShell.getMainHandler().post(new Runnable() { + public void run() { + mDocumentOverlayView.positionHandle(type, rectangle); + } + }); + } + + public RectF getCurrentCursorPosition() { + return mDocumentOverlayView.getCurrentCursorPosition(); + } + + public void setCalcHeadersController(CalcHeadersController calcHeadersController) { + mDocumentOverlayView.setCalcHeadersController(calcHeadersController); + } + + public void showCellSelection(final RectF cellCursorRect) { + LOKitShell.getMainHandler().post(new Runnable() { + public void run() { + mDocumentOverlayView.showCellSelection(cellCursorRect); + } + }); + } + + public void showHeaderSelection(final RectF cellCursorRect) { + LOKitShell.getMainHandler().post(new Runnable() { + public void run() { + mDocumentOverlayView.showHeaderSelection(cellCursorRect); + } + }); + } + + public void showAdjustLengthLine(final boolean isRow, final CalcHeadersView view) { + LOKitShell.getMainHandler().post(new Runnable() { + @Override + public void run() { + mDocumentOverlayView.showAdjustLengthLine(isRow, view); + } + }); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/android/source/src/java/org/libreoffice/overlay/DocumentOverlayView.java b/android/source/src/java/org/libreoffice/overlay/DocumentOverlayView.java new file mode 100644 index 000000000..086108cd9 --- /dev/null +++ b/android/source/src/java/org/libreoffice/overlay/DocumentOverlayView.java @@ -0,0 +1,552 @@ +/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.libreoffice.overlay; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import org.libreoffice.LibreOfficeMainActivity; +import org.libreoffice.R; +import org.libreoffice.canvas.AdjustLengthLine; +import org.libreoffice.canvas.CalcSelectionBox; +import org.libreoffice.canvas.Cursor; +import org.libreoffice.canvas.GraphicSelection; +import org.libreoffice.canvas.PageNumberRect; +import org.libreoffice.canvas.SelectionHandle; +import org.libreoffice.canvas.SelectionHandleEnd; +import org.libreoffice.canvas.SelectionHandleMiddle; +import org.libreoffice.canvas.SelectionHandleStart; +import org.mozilla.gecko.gfx.ImmutableViewportMetrics; +import org.mozilla.gecko.gfx.LayerView; +import org.mozilla.gecko.gfx.RectUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Document overlay view is responsible for showing the client drawn overlay + * elements like cursor, selection and graphic selection, and manipulate them. + */ +public class DocumentOverlayView extends View implements View.OnTouchListener { + private static final String LOGTAG = DocumentOverlayView.class.getSimpleName(); + + private static final int CURSOR_BLINK_TIME = 500; + + private boolean mInitialized = false; + + private List<RectF> mSelections = new ArrayList<RectF>(); + private List<RectF> mScaledSelections = new ArrayList<RectF>(); + private Paint mSelectionPaint = new Paint(); + private boolean mSelectionsVisible; + + private GraphicSelection mGraphicSelection; + + private boolean mGraphicSelectionMove = false; + + private LayerView mLayerView; + + private SelectionHandle mHandleMiddle; + private SelectionHandle mHandleStart; + private SelectionHandle mHandleEnd; + + private Cursor mCursor; + + private SelectionHandle mDragHandle = null; + + private List<RectF> mPartPageRectangles; + private PageNumberRect mPageNumberRect; + private boolean mPageNumberAvailable = false; + private int previousIndex = 0; // previous page number, used to compare with the current + private CalcHeadersController mCalcHeadersController; + + private CalcSelectionBox mCalcSelectionBox; + private boolean mCalcSelectionBoxDragging; + private AdjustLengthLine mAdjustLengthLine; + private boolean mAdjustLengthLineDragging; + + public DocumentOverlayView(Context context) { + super(context); + } + + public DocumentOverlayView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public DocumentOverlayView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + /** + * Initialize the selection and cursor view. + */ + public void initialize(LayerView layerView) { + if (!mInitialized) { + setOnTouchListener(this); + mLayerView = layerView; + + mCursor = new Cursor(); + mCursor.setVisible(false); + + mSelectionPaint.setColor(Color.BLUE); + mSelectionPaint.setAlpha(50); + mSelectionsVisible = false; + + mGraphicSelection = new GraphicSelection((LibreOfficeMainActivity) getContext()); + mGraphicSelection.setVisible(false); + + postDelayed(cursorAnimation, CURSOR_BLINK_TIME); + + mHandleMiddle = new SelectionHandleMiddle((LibreOfficeMainActivity) getContext()); + mHandleStart = new SelectionHandleStart((LibreOfficeMainActivity) getContext()); + mHandleEnd = new SelectionHandleEnd((LibreOfficeMainActivity) getContext()); + + mInitialized = true; + } + } + + /** + * Change the cursor position. + * @param position - new position of the cursor + */ + public void changeCursorPosition(RectF position) { + if (RectUtils.fuzzyEquals(mCursor.mPosition, position)) { + return; + } + mCursor.mPosition = position; + + ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics(); + repositionWithViewport(metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor); + } + + /** + * Change the text selection rectangles. + * @param selectionRects - list of text selection rectangles + */ + public void changeSelections(List<RectF> selectionRects) { + mSelections = selectionRects; + + ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics(); + repositionWithViewport(metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor); + } + + /** + * Change the graphic selection rectangle. + * @param rectangle - new graphic selection rectangle + */ + public void changeGraphicSelection(RectF rectangle) { + if (RectUtils.fuzzyEquals(mGraphicSelection.mRectangle, rectangle)) { + return; + } + + mGraphicSelection.mRectangle = rectangle; + + ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics(); + repositionWithViewport(metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor); + } + + public void repositionWithViewport(float x, float y, float zoom) { + RectF rect = convertToScreen(mCursor.mPosition, x, y, zoom); + mCursor.reposition(rect); + + rect = convertToScreen(mHandleMiddle.mDocumentPosition, x, y, zoom); + mHandleMiddle.reposition(rect.left, rect.bottom); + + rect = convertToScreen(mHandleStart.mDocumentPosition, x, y, zoom); + mHandleStart.reposition(rect.left, rect.bottom); + + rect = convertToScreen(mHandleEnd.mDocumentPosition, x, y, zoom); + mHandleEnd.reposition(rect.left, rect.bottom); + + mScaledSelections.clear(); + for (RectF selection : mSelections) { + RectF scaledSelection = convertToScreen(selection, x, y, zoom); + mScaledSelections.add(scaledSelection); + } + + if (mCalcSelectionBox != null) { + rect = convertToScreen(mCalcSelectionBox.mDocumentPosition, x, y, zoom); + mCalcSelectionBox.reposition(rect); + } + + if (mGraphicSelection != null && mGraphicSelection.mRectangle != null) { + RectF scaledGraphicSelection = convertToScreen(mGraphicSelection.mRectangle, x, y, zoom); + mGraphicSelection.reposition(scaledGraphicSelection); + } + + invalidate(); + } + + /** + * Convert the input rectangle from document to screen coordinates + * according to current viewport data (x, y, zoom). + */ + private static RectF convertToScreen(RectF inputRect, float x, float y, float zoom) { + RectF rect = RectUtils.scale(inputRect, zoom); + rect.offset(-x, -y); + return rect; + } + + /** + * Set part page rectangles and initialize a page number rectangle object + * (canvas element). + */ + public void setPartPageRectangles (List<RectF> rectangles) { + mPartPageRectangles = rectangles; + mPageNumberRect = new PageNumberRect(); + mPageNumberAvailable = true; + } + + /** + * Drawing on canvas. + */ + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + mCursor.draw(canvas); + + if (mPageNumberAvailable) { + mPageNumberRect.draw(canvas); + } + + mHandleMiddle.draw(canvas); + mHandleStart.draw(canvas); + mHandleEnd.draw(canvas); + + if (mSelectionsVisible) { + for (RectF selection : mScaledSelections) { + canvas.drawRect(selection, mSelectionPaint); + } + } + + if (mCalcSelectionBox != null) { + mCalcSelectionBox.draw(canvas); + } + + mGraphicSelection.draw(canvas); + + if (mCalcHeadersController != null) { + mCalcHeadersController.showHeaders(); + } + + if (mAdjustLengthLine != null) { + mAdjustLengthLine.draw(canvas); + } + } + + /** + * Cursor animation function. Switch the alpha between opaque and fully transparent. + */ + private Runnable cursorAnimation = new Runnable() { + public void run() { + if (mCursor.isVisible()) { + mCursor.cycleAlpha(); + invalidate(); + } + postDelayed(cursorAnimation, CURSOR_BLINK_TIME); + } + }; + + /** + * Show the cursor on the view. + */ + public void showCursor() { + if (!mCursor.isVisible()) { + mCursor.setVisible(true); + invalidate(); + } + } + + /** + * Hide the cursor. + */ + public void hideCursor() { + if (mCursor.isVisible()) { + mCursor.setVisible(false); + invalidate(); + } + } + + /** + * Calculate and show page number according to current viewport position. + * In particular, this function compares the middle point of the + * view port with page rectangles and finds out which page the user + * is currently on. It does not update the associated canvas element + * unless there is a change of page number. + */ + public void showPageNumberRect() { + if (null == mPartPageRectangles) return; + PointF midPoint = mLayerView.getLayerClient().convertViewPointToLayerPoint(new PointF(getWidth()/2f, getHeight()/2f)); + int index = previousIndex; + // search which page the user in currently on. can enhance the search algorithm to binary search if necessary + for (RectF page : mPartPageRectangles) { + if (page.top < midPoint.y && midPoint.y < page.bottom) { + index = mPartPageRectangles.indexOf(page) + 1; + break; + } + } + // index == 0 applies to non-text document, i.e. don't show page info on non-text docs + if (index == 0) { + return; + } + // if page rectangle canvas element is not visible or the page number is changed, show + if (!mPageNumberRect.isVisible() || index != previousIndex) { + previousIndex = index; + String pageNumberString = getContext().getString(R.string.page) + " " + index + "/" + mPartPageRectangles.size(); + mPageNumberRect.setPageNumberString(pageNumberString); + mPageNumberRect.setVisible(true); + invalidate(); + } + } + + /** + * Hide page number rectangle canvas element. + */ + public void hidePageNumberRect() { + if (null == mPageNumberRect) return; + if (mPageNumberRect.isVisible()) { + mPageNumberRect.setVisible(false); + invalidate(); + } + } + + /** + * Show text selection rectangles. + */ + public void showSelections() { + if (!mSelectionsVisible) { + mSelectionsVisible = true; + invalidate(); + } + } + + /** + * Hide text selection rectangles. + */ + public void hideSelections() { + if (mSelectionsVisible) { + mSelectionsVisible = false; + invalidate(); + } + } + + /** + * Show the graphic selection on the view. + */ + public void showGraphicSelection() { + if (!mGraphicSelection.isVisible()) { + mGraphicSelectionMove = false; + mGraphicSelection.reset(); + mGraphicSelection.setVisible(true); + invalidate(); + } + } + + /** + * Hide the graphic selection. + */ + public void hideGraphicSelection() { + if (mGraphicSelection.isVisible()) { + mGraphicSelection.setVisible(false); + invalidate(); + } + } + + /** + * Handle the triggered touch event. + */ + @Override + public boolean onTouch(View view, MotionEvent event) { + PointF point = new PointF(event.getX(), event.getY()); + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + if (mAdjustLengthLine != null && !mAdjustLengthLine.contains(point.x, point.y)) { + mAdjustLengthLine.setVisible(false); + invalidate(); + } + if (mGraphicSelection.isVisible()) { + // Check if inside graphic selection was hit + if (mGraphicSelection.contains(point.x, point.y)) { + mGraphicSelectionMove = true; + mGraphicSelection.dragStart(point); + invalidate(); + return true; + } + } else { + if (mHandleStart.contains(point.x, point.y)) { + mHandleStart.dragStart(point); + mDragHandle = mHandleStart; + return true; + } else if (mHandleEnd.contains(point.x, point.y)) { + mHandleEnd.dragStart(point); + mDragHandle = mHandleEnd; + return true; + } else if (mHandleMiddle.contains(point.x, point.y)) { + mHandleMiddle.dragStart(point); + mDragHandle = mHandleMiddle; + return true; + } else if (mCalcSelectionBox != null && + mCalcSelectionBox.contains(point.x, point.y) && + !mHandleStart.isVisible()) { + mCalcSelectionBox.dragStart(point); + mCalcSelectionBoxDragging = true; + return true; + } else if (mAdjustLengthLine != null && + mAdjustLengthLine.contains(point.x, point.y)) { + mAdjustLengthLine.dragStart(point); + mAdjustLengthLineDragging = true; + return true; + } + } + } + case MotionEvent.ACTION_UP: { + if (mGraphicSelection.isVisible() && mGraphicSelectionMove) { + mGraphicSelection.dragEnd(point); + mGraphicSelectionMove = false; + invalidate(); + return true; + } else if (mDragHandle != null) { + mDragHandle.dragEnd(point); + mDragHandle = null; + } else if (mCalcSelectionBoxDragging) { + mCalcSelectionBox.dragEnd(point); + mCalcSelectionBoxDragging = false; + } else if (mAdjustLengthLineDragging) { + mAdjustLengthLine.dragEnd(point); + mAdjustLengthLineDragging = false; + invalidate(); + } + } + case MotionEvent.ACTION_MOVE: { + if (mGraphicSelection.isVisible() && mGraphicSelectionMove) { + mGraphicSelection.dragging(point); + invalidate(); + return true; + } else if (mDragHandle != null) { + mDragHandle.dragging(point); + } else if (mCalcSelectionBoxDragging) { + mCalcSelectionBox.dragging(point); + } else if (mAdjustLengthLineDragging) { + mAdjustLengthLine.dragging(point); + invalidate(); + } + } + } + return false; + } + + /** + * Change the handle document position. + * @param type - the type of the handle + * @param position - the new document position + */ + public void positionHandle(SelectionHandle.HandleType type, RectF position) { + SelectionHandle handle = getHandleForType(type); + if (RectUtils.fuzzyEquals(handle.mDocumentPosition, position)) { + return; + } + + RectUtils.assign(handle.mDocumentPosition, position); + + ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics(); + repositionWithViewport(metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor); + } + + /** + * Hide the handle. + * @param type - type of the handle + */ + public void hideHandle(SelectionHandle.HandleType type) { + SelectionHandle handle = getHandleForType(type); + if (handle.isVisible()) { + handle.setVisible(false); + invalidate(); + } + } + + /** + * Show the handle. + * @param type - type of the handle + */ + public void showHandle(SelectionHandle.HandleType type) { + SelectionHandle handle = getHandleForType(type); + if (!handle.isVisible()) { + handle.setVisible(true); + invalidate(); + } + } + + /** + * Returns the handle instance for the input type. + */ + private SelectionHandle getHandleForType(SelectionHandle.HandleType type) { + switch(type) { + case START: + return mHandleStart; + case END: + return mHandleEnd; + case MIDDLE: + return mHandleMiddle; + } + return null; + } + + public RectF getCurrentCursorPosition() { + return mCursor.mPosition; + } + + public void setCalcHeadersController(CalcHeadersController calcHeadersController) { + mCalcHeadersController = calcHeadersController; + mCalcSelectionBox = new CalcSelectionBox((LibreOfficeMainActivity) getContext()); + } + + public void showCellSelection(RectF cellCursorRect) { + if (mCalcHeadersController == null || mCalcSelectionBox == null) return; + if (RectUtils.fuzzyEquals(mCalcSelectionBox.mDocumentPosition, cellCursorRect)) { + return; + } + + // show selection on main GL view (i.e. in the document) + RectUtils.assign(mCalcSelectionBox.mDocumentPosition, cellCursorRect); + mCalcSelectionBox.setVisible(true); + + ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics(); + repositionWithViewport(metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor); + + // show selection on headers + if (!mCalcHeadersController.pendingRowOrColumnSelectionToShowUp()) { + showHeaderSelection(cellCursorRect); + } else { + mCalcHeadersController.setPendingRowOrColumnSelectionToShowUp(false); + } + } + + public void showHeaderSelection(RectF rect) { + if (mCalcHeadersController == null) return; + mCalcHeadersController.showHeaderSelection(rect); + } + + public void showAdjustLengthLine(boolean isRow, final CalcHeadersView view) { + mAdjustLengthLine = new AdjustLengthLine((LibreOfficeMainActivity) getContext(), view, isRow, getWidth(), getHeight()); + ImmutableViewportMetrics metrics = mLayerView.getViewportMetrics(); + RectF position = convertToScreen(mCalcSelectionBox.mDocumentPosition, metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor); + mAdjustLengthLine.setScreenRect(position); + mAdjustLengthLine.setVisible(true); + invalidate(); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |