1 package org
.libreoffice
;
3 import android
.content
.Intent
;
4 import android
.graphics
.PointF
;
5 import android
.graphics
.RectF
;
6 import android
.net
.Uri
;
7 import android
.util
.Log
;
9 import org
.libreoffice
.canvas
.SelectionHandle
;
10 import org
.libreoffice
.kit
.Document
;
11 import org
.libreoffice
.overlay
.DocumentOverlay
;
12 import org
.mozilla
.gecko
.gfx
.GeckoLayerClient
;
14 import java
.util
.ArrayList
;
15 import java
.util
.Collections
;
16 import java
.util
.List
;
19 * Parses (interprets) and handles invalidation messages from LibreOffice.
21 public class InvalidationHandler
implements Document
.MessageCallback
{
22 private static String LOGTAG
= InvalidationHandler
.class.getSimpleName();
23 private final DocumentOverlay mDocumentOverlay
;
24 private final GeckoLayerClient mLayerClient
;
25 private OverlayState mState
;
26 private boolean mKeyEvent
= false;
28 public InvalidationHandler(LibreOfficeMainActivity mainActivity
) {
29 mDocumentOverlay
= mainActivity
.getDocumentOverlay();
30 mLayerClient
= LibreOfficeMainActivity
.getLayerClient();
31 mState
= OverlayState
.NONE
;
35 * Processes callback message
37 * @param messageID - ID of the message
38 * @param payload - additional invalidation message payload
41 public void messageRetrieved(int messageID
, String payload
) {
42 if (!LOKitShell
.isEditingEnabled()) {
43 // enable handling of hyperlinks even in the Viewer
44 if (messageID
!= Document
.CALLBACK_INVALIDATE_TILES
&& messageID
!= Document
.CALLBACK_HYPERLINK_CLICKED
)
48 case Document
.CALLBACK_INVALIDATE_TILES
:
49 invalidateTiles(payload
);
51 case Document
.CALLBACK_INVALIDATE_VISIBLE_CURSOR
:
52 invalidateCursor(payload
);
54 case Document
.CALLBACK_TEXT_SELECTION
:
55 textSelection(payload
);
57 case Document
.CALLBACK_TEXT_SELECTION_START
:
58 textSelectionStart(payload
);
60 case Document
.CALLBACK_TEXT_SELECTION_END
:
61 textSelectionEnd(payload
);
63 case Document
.CALLBACK_CURSOR_VISIBLE
:
64 cursorVisibility(payload
);
66 case Document
.CALLBACK_GRAPHIC_SELECTION
:
67 graphicSelection(payload
);
69 case Document
.CALLBACK_HYPERLINK_CLICKED
:
70 if (!payload
.startsWith("http://") && !payload
.startsWith("https://")) {
71 payload
= "http://" + payload
;
73 Intent urlIntent
= new Intent(Intent
.ACTION_VIEW
);
74 urlIntent
.setData(Uri
.parse(payload
));
75 LibreOfficeMainActivity
.mAppContext
.startActivity(urlIntent
);
77 case Document
.CALLBACK_STATE_CHANGED
:
78 stateChanged(payload
);
81 Log
.d(LOGTAG
, "LOK_CALLBACK uncatched: " + messageID
+ " : " + payload
);
85 private void stateChanged(String payload
) {
86 String
[] parts
= payload
.split("=");
87 if (parts
.length
< 2) {
88 Log
.e(LOGTAG
, "LOK_CALLBACK_STATE_CHANGED unexpected payload: " + payload
);
91 final String value
= parts
[1];
92 boolean pressed
= Boolean
.parseBoolean(value
);
94 if (parts
[0].equals(".uno:Bold")) {
95 LOKitShell
.getFormattingController().onToggleStateChanged(Document
.BOLD
, pressed
);
96 } else if (parts
[0].equals(".uno:Italic")) {
97 LOKitShell
.getFormattingController().onToggleStateChanged(Document
.ITALIC
, pressed
);
98 } else if (parts
[0].equals(".uno:Underline")) {
99 LOKitShell
.getFormattingController().onToggleStateChanged(Document
.UNDERLINE
, pressed
);
100 } else if (parts
[0].equals(".uno:Strikeout")) {
101 LOKitShell
.getFormattingController().onToggleStateChanged(Document
.STRIKEOUT
, pressed
);
102 } else if (parts
[0].equals(".uno:CharFontName")) {
103 LOKitShell
.getFontController().selectFont(value
);
104 } else if (parts
[0].equals(".uno:FontHeight")) {
105 LOKitShell
.getFontController().selectFontSize(value
);
106 } else if (parts
[0].equals(".uno:LeftPara")) {
107 LOKitShell
.getFormattingController().onToggleStateChanged(Document
.ALIGN_LEFT
, pressed
);
108 } else if (parts
[0].equals(".uno:CenterPara")) {
109 LOKitShell
.getFormattingController().onToggleStateChanged(Document
.ALIGN_CENTER
, pressed
);
110 } else if (parts
[0].equals(".uno:RightPara")) {
111 LOKitShell
.getFormattingController().onToggleStateChanged(Document
.ALIGN_RIGHT
, pressed
);
112 } else if (parts
[0].equals(".uno:JustifyPara")) {
113 LOKitShell
.getFormattingController().onToggleStateChanged(Document
.ALIGN_JUSTIFY
, pressed
);
115 Log
.d(LOGTAG
, "LOK_CALLBACK_STATE_CHANGED type uncatched: " + payload
);
120 * Parses the payload text with rectangle coordinates and converts to rectangle in pixel coordinates
122 * @param payload - invalidation message payload text
123 * @return rectangle in pixel coordinates
125 private RectF
convertPayloadToRectangle(String payload
) {
126 String payloadWithoutWhitespace
= payload
.replaceAll("\\s", ""); // remove all whitespace from the string
128 if (payloadWithoutWhitespace
.isEmpty() || payloadWithoutWhitespace
.equals("EMPTY")) {
132 String
[] coordinates
= payloadWithoutWhitespace
.split(",");
134 if (coordinates
.length
!= 4) {
138 int x
= Integer
.decode(coordinates
[0]);
139 int y
= Integer
.decode(coordinates
[1]);
140 int width
= Integer
.decode(coordinates
[2]);
141 int height
= Integer
.decode(coordinates
[3]);
143 float dpi
= LOKitShell
.getDpi();
146 LOKitTileProvider
.twipToPixel(x
, dpi
),
147 LOKitTileProvider
.twipToPixel(y
, dpi
),
148 LOKitTileProvider
.twipToPixel(x
+ width
, dpi
),
149 LOKitTileProvider
.twipToPixel(y
+ height
, dpi
)
154 * Parses the payload text with more rectangles (separated by ';') and converts to a list of rectangles.
156 * @param payload - invalidation message payload text
157 * @return list of rectangles
159 private List
<RectF
> convertPayloadToRectangles(String payload
) {
160 List
<RectF
> rectangles
= new ArrayList
<RectF
>();
161 String
[] rectangleArray
= payload
.split(";");
163 for (String coordinates
: rectangleArray
) {
164 RectF rectangle
= convertPayloadToRectangle(coordinates
);
165 if (rectangle
!= null) {
166 rectangles
.add(rectangle
);
175 * Handles the tile invalidation message
179 private void invalidateTiles(String payload
) {
180 RectF rectangle
= convertPayloadToRectangle(payload
);
181 if (rectangle
!= null) {
182 LOKitShell
.sendTileInvalidationRequest(rectangle
);
187 * Handles the cursor invalidation message
191 private synchronized void invalidateCursor(String payload
) {
192 RectF cursorRectangle
= convertPayloadToRectangle(payload
);
193 if (cursorRectangle
!= null) {
194 mDocumentOverlay
.positionCursor(cursorRectangle
);
195 mDocumentOverlay
.positionHandle(SelectionHandle
.HandleType
.MIDDLE
, cursorRectangle
);
197 if (mState
== OverlayState
.TRANSITION
|| mState
== OverlayState
.CURSOR
) {
198 changeStateTo(OverlayState
.CURSOR
);
202 moveViewportToMakeCursorVisible(cursorRectangle
);
209 * Move the viewport to show the cursor. The cursor will appear at the
210 * viewport position depending on where the cursor is relative to the
211 * viewport (either cursor is above, below, on left or right).
213 * @param cursorRectangle - cursor position on the document
215 public void moveViewportToMakeCursorVisible(RectF cursorRectangle
) {
216 RectF moveToRect
= mLayerClient
.getViewportMetrics().getCssViewport();
217 if (moveToRect
.contains(cursorRectangle
)) {
221 float newLeft
= moveToRect
.left
;
222 float newTop
= moveToRect
.top
;
224 if (cursorRectangle
.right
< moveToRect
.left
|| cursorRectangle
.left
< moveToRect
.left
) {
225 newLeft
= cursorRectangle
.left
- (moveToRect
.width() * 0.1f
);
226 } else if (cursorRectangle
.right
> moveToRect
.right
|| cursorRectangle
.left
> moveToRect
.right
) {
227 newLeft
= cursorRectangle
.right
- (moveToRect
.width() * 0.9f
);
230 if (cursorRectangle
.top
< moveToRect
.top
|| cursorRectangle
.bottom
< moveToRect
.top
) {
231 newTop
= cursorRectangle
.top
- (moveToRect
.height() * 0.1f
);
232 } else if (cursorRectangle
.bottom
> moveToRect
.bottom
|| cursorRectangle
.top
> moveToRect
.bottom
) {
233 newTop
= cursorRectangle
.bottom
- (moveToRect
.height() / 2.0f
);
236 LOKitShell
.moveViewportTo(new PointF(newLeft
, newTop
), null);
240 * Handles the text selection start message
244 private synchronized void textSelectionStart(String payload
) {
245 RectF selectionRect
= convertPayloadToRectangle(payload
);
246 if (selectionRect
!= null) {
247 mDocumentOverlay
.positionHandle(SelectionHandle
.HandleType
.START
, selectionRect
);
252 * Handles the text selection end message
256 private synchronized void textSelectionEnd(String payload
) {
257 RectF selectionRect
= convertPayloadToRectangle(payload
);
258 if (selectionRect
!= null) {
259 mDocumentOverlay
.positionHandle(SelectionHandle
.HandleType
.END
, selectionRect
);
264 * Handles the text selection message
268 private synchronized void textSelection(String payload
) {
269 if (payload
.isEmpty() || payload
.equals("EMPTY")) {
270 if (mState
== OverlayState
.SELECTION
) {
271 changeStateTo(OverlayState
.TRANSITION
);
273 mDocumentOverlay
.changeSelections(Collections
.EMPTY_LIST
);
275 List
<RectF
> rectangles
= convertPayloadToRectangles(payload
);
276 if (mState
!= OverlayState
.SELECTION
) {
277 changeStateTo(OverlayState
.TRANSITION
);
279 changeStateTo(OverlayState
.SELECTION
);
280 mDocumentOverlay
.changeSelections(rectangles
);
285 * Handles the cursor visibility message
289 private synchronized void cursorVisibility(String payload
) {
290 if (payload
.equals("true")) {
291 mDocumentOverlay
.showCursor();
292 if (mState
!= OverlayState
.SELECTION
) {
293 mDocumentOverlay
.showHandle(SelectionHandle
.HandleType
.MIDDLE
);
295 } else if (payload
.equals("false")) {
296 mDocumentOverlay
.hideCursor();
297 mDocumentOverlay
.hideHandle(SelectionHandle
.HandleType
.MIDDLE
);
302 * Handles the graphic selection change message
306 private void graphicSelection(String payload
) {
307 if (payload
.isEmpty() || payload
.equals("EMPTY")) {
308 if (mState
== OverlayState
.GRAPHIC_SELECTION
) {
309 changeStateTo(OverlayState
.TRANSITION
);
312 RectF rectangle
= convertPayloadToRectangle(payload
);
313 mDocumentOverlay
.changeGraphicSelection(rectangle
);
314 if (mState
!= OverlayState
.GRAPHIC_SELECTION
) {
315 changeStateTo(OverlayState
.TRANSITION
);
317 changeStateTo(OverlayState
.GRAPHIC_SELECTION
);
322 * Trigger a transition to a new overlay state.
324 * @param next - new state to transition to
326 public synchronized void changeStateTo(OverlayState next
) {
327 changeState(mState
, next
);
331 * Executes a transition from old overlay state to a new overlay state.
333 * @param previous - old state
334 * @param next - new state
336 private synchronized void changeState(OverlayState previous
, OverlayState next
) {
338 handleGeneralChangeState(previous
, next
);
341 handleCursorState(previous
);
344 handleSelectionState(previous
);
346 case GRAPHIC_SELECTION
:
347 handleGraphicSelectionState(previous
);
350 handleTransitionState(previous
);
353 handleNoneState(previous
);
359 * Handle a general transition - executed for all transitions.
361 private void handleGeneralChangeState(OverlayState previous
, OverlayState next
) {
362 if (previous
== OverlayState
.NONE
) {
363 LOKitShell
.getToolbarController().switchToEditMode();
364 } else if (next
== OverlayState
.NONE
) {
365 LOKitShell
.getToolbarController().switchToViewMode();
370 * Handle a transition to OverlayState.NONE state.
372 private void handleNoneState(OverlayState previous
) {
373 if (previous
== OverlayState
.NONE
) {
377 // Just hide everything
378 mDocumentOverlay
.hideHandle(SelectionHandle
.HandleType
.START
);
379 mDocumentOverlay
.hideHandle(SelectionHandle
.HandleType
.END
);
380 mDocumentOverlay
.hideHandle(SelectionHandle
.HandleType
.MIDDLE
);
381 mDocumentOverlay
.hideSelections();
382 mDocumentOverlay
.hideCursor();
383 mDocumentOverlay
.hideGraphicSelection();
384 LibreOfficeMainActivity
.mAppContext
.hideSoftKeyboard();
388 * Handle a transition to OverlayState.SELECTION state.
390 private void handleSelectionState(OverlayState previous
) {
391 mDocumentOverlay
.showHandle(SelectionHandle
.HandleType
.START
);
392 mDocumentOverlay
.showHandle(SelectionHandle
.HandleType
.END
);
393 mDocumentOverlay
.showSelections();
397 * Handle a transition to OverlayState.CURSOR state.
399 private void handleCursorState(OverlayState previous
) {
400 LibreOfficeMainActivity
.mAppContext
.showSoftKeyboardOrFormattingToolbar();
401 if (previous
== OverlayState
.TRANSITION
) {
402 mDocumentOverlay
.showHandle(SelectionHandle
.HandleType
.MIDDLE
);
403 mDocumentOverlay
.showCursor();
408 * Handle a transition to OverlayState.TRANSITION state.
410 private void handleTransitionState(OverlayState previous
) {
411 if (previous
== OverlayState
.SELECTION
) {
412 mDocumentOverlay
.hideHandle(SelectionHandle
.HandleType
.START
);
413 mDocumentOverlay
.hideHandle(SelectionHandle
.HandleType
.END
);
414 mDocumentOverlay
.hideSelections();
415 } else if (previous
== OverlayState
.CURSOR
) {
416 mDocumentOverlay
.hideHandle(SelectionHandle
.HandleType
.MIDDLE
);
417 } else if (previous
== OverlayState
.GRAPHIC_SELECTION
) {
418 mDocumentOverlay
.hideGraphicSelection();
423 * Handle a transition to OverlayState.GRAPHIC_SELECTION state.
425 private void handleGraphicSelectionState(OverlayState previous
) {
426 mDocumentOverlay
.showGraphicSelection();
427 LibreOfficeMainActivity
.mAppContext
.hideSoftKeyboard();
431 * The current state the overlay is in.
433 public OverlayState
getCurrentState() {
438 * A key event happened (i.e. user started typing).
440 public void keyEvent() {
445 * The states the overlay.
447 public enum OverlayState
{
449 * State where the overlay is empty
453 * In-between state where we need to transition to a new overlay state.
454 * In this state we properly disable the older state and wait to transition
455 * to a new state triggered by an invalidation.
459 * State where we operate with the cursor.
463 * State where we operate the graphic selection.
467 * State where we operate the text selection.
473 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */