package dasherJava.core.output;
import dasherJava.core.output.ATSPILibrary.ATSPIAction;
import dasherJava.core.output.ATSPILibrary.ATSPIEditableText;
import dasherJava.core.output.ATSPILibrary.ATSPIEventListener;
import dasherJava.core.output.ATSPILibrary.ATSPIEventListenerCallback;
import dasherJava.core.output.ATSPILibrary.ATSPIText;
import dasherJava.core.output.ATSPILibrary.ATSPITextRange;
public class ATSPIImplementation implements TextCharOutput, AccessibilityInterface {
private static final Object CLASS_INIT_MONITOR = new Object();
private static final Object CURRENT_TEXT_MONITOR = new Object();
private static final Object CURRENT_ACTION_MONITOR = new Object();
//keep these references to prevent the objects from being garbage-collected, which would cause
//crashes if they were then accessed by native code
private static ATSPIEventListenerCallback focusCallback;
private static ATSPIEventListenerCallback caretCallback;
private static volatile ATSPIEventListener focusListener;
private static volatile ATSPIEventListener caretListener;
private static boolean classInitialized = false;
private volatile AccessibilityActionsListTarget listTarget;
private ATSPIText currentText;
private ATSPIEditableText currentEditableText;
private ATSPIAction currentAction;
//To be consistent with all other input and output implementations, we provide a constructor for initializing
//this interface and a terminate() method that should be called once it is not needed anymore. However,
//initializing and especially terminating the AT-SPI is complicated, so we do most initialization on class-level
//the first time the constructor is called and only unregister the listeners (instead of fully deleting
//everything) when asked to terminate. There is an additional exit() method that completely exits the AT-SPI,
//but it cannot be re-initialized after this method has been called.
public ATSPIImplementation() throws UnsatisfiedLinkError {
doClassInit();
ATSPILibrary.get().atspi_event_listener_register(focusListener, "object:state-changed:focused", null);
ATSPILibrary.get().atspi_event_listener_register(caretListener, "object:text-caret-moved", null);
}
private void doClassInit() throws UnsatisfiedLinkError {
synchronized (CLASS_INIT_MONITOR) {
if (classInitialized) return; //already initialized
ATSPILibrary.get().atspi_init();
focusCallback=(event, user_data) -> {
synchronized (CURRENT_TEXT_MONITOR) {
currentText=ATSPILibrary.get().atspi_accessible_get_text_iface(event.source);
currentEditableText=ATSPILibrary.get().atspi_accessible_get_editable_text_iface(event.source);
}
synchronized (CURRENT_ACTION_MONITOR) {
currentAction=ATSPILibrary.get().atspi_accessible_get_action_iface(event.source);
AccessibilityActionsListTarget target = listTarget; //for atomicity
if (target!=null) target.receive(getAvailableActions());
}
};
caretCallback=(event, user_data) -> {
synchronized (CURRENT_TEXT_MONITOR) {
currentText=ATSPILibrary.get().atspi_accessible_get_text_iface(event.source);
currentEditableText=ATSPILibrary.get().atspi_accessible_get_editable_text_iface(event.source);
}
};
focusListener=ATSPILibrary.get().atspi_event_listener_new(focusCallback, null, null);
caretListener=ATSPILibrary.get().atspi_event_listener_new(caretCallback, null, null);
new Thread(() -> ATSPILibrary.get().atspi_event_main(), "AT-SPI event loop").start();
classInitialized=true;
}
}
@Override
public void outputChar(int unicode) {
ATSPILibrary.get().atspi_generate_keyboard_event(ATSPILibrary.NATIVE_LONG_ZERO, Character.toString(unicode),
ATSPILibrary.ATSPI_KEY_STRING, null);
}
@Override
public void deleteLastChar() {
ATSPILibrary.get().atspi_generate_keyboard_event(ATSPILibrary.KEYSYM_BACKSPACE, null,
ATSPILibrary.ATSPI_KEY_SYM, null);
}
@Override
public void deleteText(TextRange range) {
synchronized (CURRENT_TEXT_MONITOR) {
if (currentText==null || currentEditableText==null) return;
if (range==TextRange.TEXT_RANGE_ALL) {
int textLength = ATSPILibrary.get().atspi_text_get_character_count(currentText, null);
if (textLength<=0) return; //already empty
ATSPILibrary.get().atspi_editable_text_delete_text(currentEditableText, 0, textLength, null);
return;
}
int caretOffset = ATSPILibrary.get().atspi_text_get_caret_offset(currentText, null);
ATSPITextRange atspiTextRange = ATSPILibrary.get().atspi_text_get_string_at_offset(currentText,
caretOffset, getGranularity(range), null);
ATSPILibrary.get().atspi_editable_text_delete_text(currentEditableText, atspiTextRange.start_offset,
atspiTextRange.end_offset, null);
}
}
@Override
public void moveTextCaret(TextTarget target) {
synchronized (CURRENT_TEXT_MONITOR) {
if (currentText==null) return;
int caretOffset = ATSPILibrary.get().atspi_text_get_caret_offset(currentText, null);
int targetOffset;
if (target==TextTarget.TEXT_TARGET_START) targetOffset=0;
else if (target==TextTarget.TEXT_TARGET_END) {
int textLength = ATSPILibrary.get().atspi_text_get_character_count(currentText, null);
if (textLength<=0) return; //text is empty
targetOffset=textLength;
} else if (isForwards(target)) {
ATSPITextRange range = ATSPILibrary.get().atspi_text_get_text_after_offset(currentText, caretOffset,
getBoundaryType(target), null);
targetOffset=range.start_offset;
} else { //backwards
ATSPITextRange range = ATSPILibrary.get().atspi_text_get_text_before_offset(currentText, caretOffset,
getBoundaryType(target), null);
targetOffset=range.start_offset;
}
ATSPILibrary.get().atspi_text_set_caret_offset(currentText, targetOffset, null);
}
}
@Override
public void doAction(String action) {
synchronized (CURRENT_ACTION_MONITOR) {
if (currentAction!=null) {
String[] availableActions = getAvailableActions();
for (int i = 0; i<availableActions.length; i++) {
if (availableActions[i].equals(action)) {
ATSPILibrary.get().atspi_action_do_action(currentAction, i, null);
return;
}
}
}
}
}
@Override
public String[] getAvailableActions() {
synchronized (CURRENT_ACTION_MONITOR) {
if (currentAction!=null) {
int numberOfActions = ATSPILibrary.get().atspi_action_get_n_actions(currentAction, null);
String[] actionNames = new String[numberOfActions];
for (int i = 0; i<numberOfActions; i++) {
actionNames[i]=ATSPILibrary.getString(ATSPILibrary.get().atspi_action_get_action_name(
currentAction, i, null));
}
return actionNames;
}
return new String[0];
}
}
@Override
public void setAccessibilityActionsListTarget(AccessibilityActionsListTarget listTarget) {
this.listTarget=listTarget;
}
@Override
public void terminate() {
ATSPILibrary.get().atspi_event_listener_deregister(focusListener, "object:state-changed:focused", null);
ATSPILibrary.get().atspi_event_listener_deregister(caretListener, "object:text-caret-moved", null);
}
public static void exit() {
synchronized (CLASS_INIT_MONITOR) {
if (!classInitialized) return; //was never initialized
//We should somehow exit the event loop thread here, but that is only possible by calling
//atspi_event_quit() from within an event handler according to the documentation. Not sure how that
//is supposed to work since we cannot trigger an event handler ourselves...
ATSPILibrary.get().atspi_exit();
}
}
private static boolean isForwards(TextTarget target) {
switch (target) {
case TEXT_TARGET_NEXT_CHAR:
case TEXT_TARGET_NEXT_WORD:
case TEXT_TARGET_NEXT_SENTENCE:
case TEXT_TARGET_NEXT_LINE:
case TEXT_TARGET_NEXT_PARAGRAPH:
return true;
case TEXT_TARGET_PREVIOUS_CHAR:
case TEXT_TARGET_PREVIOUS_WORD:
case TEXT_TARGET_PREVIOUS_SENTENCE:
case TEXT_TARGET_PREVIOUS_LINE:
case TEXT_TARGET_PREVIOUS_PARAGRAPH:
return false;
case TEXT_TARGET_START:
case TEXT_TARGET_END:
default: //null
throw new RuntimeException("isForwards(): Invalid text target: "+target);
}
}
private static int getBoundaryType(TextTarget target) {
switch (target) {
case TEXT_TARGET_PREVIOUS_CHAR:
case TEXT_TARGET_NEXT_CHAR:
return ATSPILibrary.ATSPI_TEXT_BOUNDARY_CHAR;
case TEXT_TARGET_PREVIOUS_WORD:
case TEXT_TARGET_NEXT_WORD:
return ATSPILibrary.ATSPI_TEXT_BOUNDARY_WORD_START;
case TEXT_TARGET_PREVIOUS_SENTENCE:
case TEXT_TARGET_NEXT_SENTENCE:
return ATSPILibrary.ATSPI_TEXT_BOUNDARY_SENTENCE_START;
case TEXT_TARGET_PREVIOUS_LINE:
case TEXT_TARGET_NEXT_LINE:
case TEXT_TARGET_PREVIOUS_PARAGRAPH: //not yet implemented, so currently same as line
case TEXT_TARGET_NEXT_PARAGRAPH: //not yet implemented, so currently same as line
return ATSPILibrary.ATSPI_TEXT_BOUNDARY_LINE_START;
case TEXT_TARGET_START:
case TEXT_TARGET_END:
default: //null
throw new RuntimeException("getBoundaryType(): Invalid text target: "+target);
}
}
private static int getGranularity(TextRange range) {
switch (range) {
case TEXT_RANGE_CHAR:
return ATSPILibrary.ATSPI_TEXT_GRANULARITY_CHAR;
case TEXT_RANGE_WORD:
return ATSPILibrary.ATSPI_TEXT_GRANULARITY_WORD;
case TEXT_RANGE_SENTENCE:
return ATSPILibrary.ATSPI_TEXT_GRANULARITY_SENTENCE;
case TEXT_RANGE_LINE:
return ATSPILibrary.ATSPI_TEXT_GRANULARITY_LINE;
case TEXT_RANGE_PARAGRAPH:
return ATSPILibrary.ATSPI_TEXT_GRANULARITY_PARAGRAPH;
case TEXT_RANGE_ALL:
default: //null
throw new RuntimeException("getGranularity(): Invalid text range: "+range);
}
}
}