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