<The previous article in this series | The table of contents of this series | The next article in this series>
How to Implement and Register Listeners of Spread Sheet Mouse Click Events and Keystroke Events in LibreOffice or Apache OpenOffice Is Explained
About: UNO (Universal Network Objects)
About: LibreOffice
About: Apache OpenOffice
About: Java Programming Language
Here are -Hypothesizer and -Rebutter sitting in front of a computer screen in a room on a brook among some mountains on the Bias planet.
We need to listen to spread sheet mouse click events and keystroke events, because we are using spread sheets as GUI components of our applications. That is, we aren't just handling spread sheet documents as files.
We won't necessarily need to listen to those events because we are using spread sheets as GUI components . . .
Well, we may not necessarily need to do so, but without doing so, the user interfaces of some of our applications can't avoid being very frustrating.
Some? What some?
I think, a user interface is frustrating when it requires the user to do what, the user feels, shouldn't be required. I mean, by requiring to push a button after the user clicks a cell, the application can know the occurrence of the click, but the user would feel, "Why should I be required to push the damn button just for notifying the damn application my click? I already clearly showed my intention by clicking the damn cell!"
Is your user ill-bred, using a word like 'damn' three times in just two sentences?
Actually, the user is nobody but me.
So, the answer is yes. Anyway, the button or the cell isn't a one to be damned, for they aren't ones to be blamed.
. . . I will retain 'damn' only for the application: the application is damn.
Anyway, in order to push the button, the user has to move the mouse cursor from the cell to the button, often very far, and often he or she has to move the mouse cursor from the button to another cell near the previously clicked cell. And such operations are often repetitive.
In the first place, why are we using spread sheets as GUI components?
Because the spread sheet user interface of LibreOffice is handy: we can set functions into cells, which are automatically refreshed as other cells are changed, can set fonts, colors, etc. into cells or characters, can do spell checks, can merge cells, can easily input serial data, can create charts, . . .. Creating our own user interface that parallels the spread sheet user interface of LibreOffice wouldn't be insignificant task. So, we want to take advantage of the spread sheet user interface of LibreOffice, and incorporate our logics into the user interface.
In fact, don't we want to listen to events of the current cell movement, not mouse click events or keystroke events themselves?
Yes. Our present concern is to listen to events of the current cell movement, but regrettably, we can't find out how to directly do so. We want to listen to mouse click events and keystroke events as a way to infer the current cell movement.
The same with the previous scene.
As for mouse click events, the controller implements an interface, 'com.sun.star.sheet.XEnhancedMouseClickBroadcaster', that accepts listeners that implement 'com.sun.star.awt.XEnhancedMouseClickHandler'; as for keystroke events, the controller implements an interface, 'com.sun.star.awt.XUserInputInterception', that accepts listeners that implement 'com.sun.star.awt.XKeyHandler'.
This time, we will make the 'thebiasplanet.unoextensiontests.controllers.TestController' class of the 'testUnoExtensionToDisclose' project the listener of both kinds of events, like this.
-Hypothesizer edits the 'thebiasplanet.unoextensiontests.controllers.TestController' class (he used their utility classes, 'thebiasplanet.unoutilities.documentshandling.spreadsheetsdocumentshandling.UnoSpreadSheet', 'thebiasplanet.unoutilities.documentshandling.spreadsheetsdocumentshandling.UnoSpreadSheetsDocument', and 'thebiasplanet.coreutilities.messaging.Publisher'; how to get the reference to the controller is described here; the method, 'appendToFile', appends the specified string into the specified file). -Hypothesizer also added a button, 'Set the Events Listeners', into the control panel.
package thebiasplanet.unoextensiontests.controllers;
~~~ Import directives irrelevant here are omitted. ~~~
import java.net.URL;
import com.sun.star.awt.EnhancedMouseEvent;
import com.sun.star.awt.KeyEvent;
import com.sun.star.awt.XEnhancedMouseClickHandler;
import com.sun.star.awt.XKeyHandler;
import com.sun.star.awt.XUserInputInterception;
import com.sun.star.beans.XPropertySet;
import com.sun.star.lang.EventObject;
import com.sun.star.sheet.XEnhancedMouseClickBroadcaster;
import com.sun.star.sheet.XSpreadsheetView;
import com.sun.star.table.XCell;
import com.sun.star.uno.UnoRuntime;
import com.sun.star.uno.XComponentContext;
import thebiasplanet.coreutilities.constantsgroups.*;
import thebiasplanet.coreutilities.messaging.Publisher;
import thebiasplanet.unoutilities.documentshandling.spreadsheetsdocumentshandling.UnoSpreadSheet;
import thebiasplanet.unoutilities.documentshandling.spreadsheetsdocumentshandling.UnoSpreadSheetsDocument;
public class TestController implements XEnhancedMouseClickHandler, XKeyHandler {
private XComponentContext i_componentContextInXComponentContext;
private String i_openedDocumentDirectoryPath;
public TestController (XComponentContext a_componentContextInXComponentContext) {
i_componentContextInXComponentContext = a_componentContextInXComponentContext;
}
~~~ Parts irrelevant here are omitted. ~~~
public void setEventsListeners () throws Exception {
UnoSpreadSheet l_currentSpreadSheet = UnoSpreadSheet.getCurrentSpreadSheet (i_componentContextInXComponentContext);
if (l_currentSpreadSheet != null) {
UnoSpreadSheetsDocument l_currentSpreadSheetsDocument = l_currentSpreadSheet.getSpreadSheetsDocument ();
XSpreadsheetView l_controllerInXSpreadsheetView = l_currentSpreadSheetsDocument.getControllerInXSpreadsheetView ();
XEnhancedMouseClickBroadcaster l_controllerInXEnhancedMouseClickBroadcaster = (XEnhancedMouseClickBroadcaster) UnoRuntime.queryInterface (XEnhancedMouseClickBroadcaster.class, l_controllerInXSpreadsheetView);
l_controllerInXEnhancedMouseClickBroadcaster.addEnhancedMouseClickHandler (this);
XUserInputInterception l_controllerInXUserInputInterception = (XUserInputInterception) UnoRuntime.queryInterface (XUserInputInterception.class, l_controllerInXSpreadsheetView);
l_controllerInXUserInputInterception.addKeyHandler (this);
String l_openedDocumentFilePath = (new URL (l_currentSpreadSheetsDocument.getLocationUrl ())).getFile ();
i_openedDocumentDirectoryPath = l_openedDocumentFilePath.substring (0, l_openedDocumentFilePath.lastIndexOf ("/"));
}
}
private String getCellAbsoluteName (XCell a_cellInXCell) {
XPropertySet l_cellInXPropertySet = (XPropertySet) UnoRuntime.queryInterface (XPropertySet.class, a_cellInXCell);
try {
return (String) l_cellInXPropertySet.getPropertyValue ("AbsoluteName");
}
catch (Exception l_exception) {
// This will never happen.
return null;
}
}
@Override
public void disposing (EventObject a_event) {
}
@Override
public boolean mousePressed (EnhancedMouseEvent a_enhancedMouseEvent) {
XCell l_mousePressedCellInXCell = (XCell) UnoRuntime.queryInterface (XCell.class, a_enhancedMouseEvent.Target);
if (l_mousePressedCellInXCell != null) {
Publisher.appendToFile ( String.format ("%s/%s", i_openedDocumentDirectoryPath, "UnoMouseEvents.txt"), String.format ("MousePressed: on %s, buttons = %d, modifiers = %d, count = %d", getCellAbsoluteName (l_mousePressedCellInXCell), a_enhancedMouseEvent.Buttons, a_enhancedMouseEvent.Modifiers, a_enhancedMouseEvent.ClickCount));
}
return true;
}
@Override
public boolean mouseReleased (EnhancedMouseEvent a_enhancedMouseEvent) {
XCell l_mouseReleasedCellInXCell = (XCell) UnoRuntime.queryInterface (XCell.class, a_enhancedMouseEvent.Target);
if (l_mouseReleasedCellInXCell != null) {
Publisher.appendToFile ( String.format ("%s/%s", i_openedDocumentDirectoryPath, "UnoMouseEvents.txt"), String.format ("MouseReleased: on %s, buttons = %d, modifiers = %d, count = %d", getCellAbsoluteName (l_mouseReleasedCellInXCell), a_enhancedMouseEvent.Buttons, a_enhancedMouseEvent.Modifiers, a_enhancedMouseEvent.ClickCount));
}
return true;
}
@Override
public boolean keyPressed (KeyEvent a_keyEvent) {
Publisher.appendToFile (String.format ("%s/%s", i_openedDocumentDirectoryPath, "UnoKeyEvents.txt"), String.format ("KeyPressed: code = %d, modifiers = %d", a_keyEvent.KeyCode, a_keyEvent.Modifiers));
return false;
}
@Override
public boolean keyReleased (KeyEvent a_keyEvent) {
Publisher.appendToFile (String.format ("%s/%s", i_openedDocumentDirectoryPath, "UnoKeyEvents.txt"), String.format ("KeyReleased: code = %d, modifiers = %d", a_keyEvent.KeyCode, a_keyEvent.Modifiers));
return false;
}
}
As you can see, from the event arguments of the 'mousePressed' method and the 'mouseReleased' method, we can get the cell on which the mouse event happened.
What if the mouse click happened not on a cell?
. . . What do you mean? Do you mean that the mouse click happened precisely on the border between two cells?
I don't assume that a mouse click can happen precisely on a border, but I know that a row label box (I don't know the official name of it. I mean a box that shows the row number, by clicking on which we can select the whole row), a column label box (please guess what it is from the explanation of 'row label box'), or the 'select-all-the-cells-box' (what is the official name of it? I guess what I mean will be understood) can be clicked.
Well, . . . let's find out.
-Hypothesizer does some experiments.
. . . Really?
There doesn't seem to be any reason to doubt its being real.
Our listener doesn't hear anything when a row label box, a column label box, or the 'select-all-the-cells-box' is clicked. . . .Such a behavior seems unreasonable, for many people who want to hear cell clicks will also want to hear row label box clicks.
Is that a problem?
That's a huge, or, I would say, critical problem! As mouse events on row label boxs move the current cell, we have to detect such mouse events in order to infer the movement of the current cell.
Isn't there a way to infer the movement of the current cell without detecting such mouse events?
There isn't any. I thought of Inferring it from the change of the selection, but that's impossible. For example, there are these two cases in which the changes of the selection are the same but the movements of the current cell are different.
In the first case, we press (and not release) the mouse button on the 9th row label box, drag the mouse cursor to the 7th row, drag back the mouse cursor to the 8th row, and release the button. The changes of the selection are 'A9:AMJ9' -> 'A7:AMJ9' -> 'A8:AMJ9'; the last current cell is at 'A8'.
At 'A8'? That behavior feels inconsistent with the behavior of when the mouse cursor is dragged over cells: in the latter, the current cell doesn't move as the mouse cursor is dragged.
Certainly, but that's the way it really is.
In the second case, we press (and not release) the mouse button on the 9th row label box, drag the mouse cursor to the 7th row, release the button, and click with the control key pressed on the 7th row label box. The changes of the selection are 'A9:AMJ9' -> 'A7:AMJ9' -> 'A8:AMJ9', the same with the first case; the last current cell is at 'A7', different from the first case.
Well, when 'select-all-the-cells-box' is clicked, where does the current cell go?
In fact, it doesn't go anywhere.
That behavior also feels inconsistent with the behavior of when a row or column label box is clicked. Why doesn't the current cell move in the former, while it does in the latter?
I don't know, but that's the way it is.
Well, isn't there another type of mouse events listener we can use?
Hmm, the interface, 'com.sun.star.awt.XUserInputInterception', also accepts mouse clicks listeners, and the interface, 'XWindow', implemented by the spread sheet window also accepts mouse clicks listeners. . . . I wonder whether those listeners can hear what we want to hear.
-Hypothesizer does some experiments with those listeners, taking some time.
. . . You know what, those listeners can't hear what we want to hear, and especially, the latter doesn't hear anything.
Why?
I don't know. Probably, events are intercepted and consumed by another listener.
Well, we don't have to blindly obey the way it is, at least when the way it is is inconsistent or unreasonable, do we?
. . . Being said "blindly obey", as far as the current cell moves, we will have to infer the movement as it does . . .
I thought we could move the current cell from our programs.
Ah, . . . so, we would control the current cell movement, not just infer it?
If possible.
In fact, we don't know how to move only the current cell without any side effect: the existing selection will be nullified by our moving the current cell.
Can't we reselect the previous selection area from our programs?
Well, . . . we could, but that seems too much: it feels like we are sinking into a bottomless swamp, just for rather infrequent cases . . .
Well, giving up on some infrequent cases can be a viable solution, although it may be somewhat ugly.
I will consider what to do . . .
As for keystroke events, how can we know the cell that received the keystroke?
As far as I look at the event argument, we seem to not be able to know that.
Hmm, . . . is that OK?
Well, I thought that if we successfully managed to infer the position of the current cell, that cell would be the cell that received the keystroke. However, that precondition seems to have to be conditional.
What are those return values of the listener methods?
Actually, there is no explanation for the return value of the 'mousePressed' method or the 'mouseReleased' method in the API document, but the return value of 'false' seems to mean 'not forwarding the event'.
No explanation at all?
The API document says that the return type is boolean, and that's the whole of the explanation, unless some kind of secret ink is used.
What exactly will happen if the method returns 'false'?
The current cell won't move to the clicked cell, if, of course, we don't move it from our method. So, the user will feel as though his or her click was ignored.
Why do we return 'false' for the 'keyPressed' method and the 'keyReleased' method?
Because for those methods, returning 'true' means 'not forwarding the event'.
So, is the meaning of the return of the 'mousePressed' method opposite to that of the 'keyPressed' method?
I guessed so.
Are those events fired before the selection change events are fired?
Yes. As we can know from the fact that we can block the movement of the current cell by the return values, at the time when those methods are called, the selection isn't changed yet.
Our test code is included in this zip file, and how to use the zip file is described in this article. Opening the 'TestSpreadSheetsDocumentForExecutingTests.ods' file, clicking the 'Show Test Control Panel' button, and clicking the 'Set the Events Listeners' button will begin listening to events. Events detected will be recorded into the files, 'UnoMouseEvents.txt' or 'UnoKeyEvents.txt', in the 'execution' directory of the 'testUnoExtensionToDisclose' project.
- Apache OpenOffice Wiki. (2014/01/02). Apache OpenOffice Developer's Guide. Retrieved from https://wiki.openoffice.org/wiki/Documentation/DevGuide/OpenOffice.org_Developers_Guide
<The previous article in this series | The table of contents of this series | The next article in this series>