DasherJava / src / dasherJava / core / output / ATSPIImplementation.java
ATSPIImplementation.java
Raw
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);
		}
	}
}