/* -*- 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 mSelections = new ArrayList(); private List mScaledSelections = new ArrayList(); 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 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 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 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: */