Blob Blame History Raw
/*
 * Java ATK Wrapper for GNOME
 * Copyright (C) 2009 Sun Microsystems Inc.
 * Copyright (C) 2015 Magdalen Berns <m.berns@thismagpie.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

package org.GNOME.Accessibility;

import java.awt.*;
import java.awt.event.*;
import java.beans.*;
import java.io.*;
import javax.accessibility.*;
import java.awt.Toolkit;

public class AtkWrapper {
  static boolean accessibilityEnabled = false;
  static {
    try {
      Process p = Runtime.getRuntime().exec("@XPROP@ -root");
      BufferedReader b = new BufferedReader(new InputStreamReader (p.getInputStream ()));
      String result;
      while ((result = b.readLine()) != null) {
        if (result.indexOf("AT_SPI_IOR") >= 0 || result.indexOf("AT_SPI_BUS") >= 0) {
          System.loadLibrary("atk-wrapper");
          if (AtkWrapper.initNativeLibrary())
            accessibilityEnabled = true;
          break;
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
      e.getCause();
    }
  }

 /**
  * winAdapter
  *
  * <http://docs.oracle.com/javase/8/docs/api/java/awt/event/WindowAdapter.html>
  * <http://docs.oracle.com/javase/8/docs/api/java/awt/event/WindowEvent.html>
  */
  final WindowAdapter winAdapter = new WindowAdapter() {

  /**
    * windowActivated:
    *                    Invoked when a Window becomes the active Window.
    *                    Only a Frame or a Dialog can be the active Window. The
    *                    native windowing system may denote the active Window
    *                    or its children with special decorations, such as a
    *                    highlighted title bar. The active Window is always either
    *                    the focused Window, or the first Frame or Dialog that is
    *                    an owner of the focused Window.
    * @param e A WindowEvent object
    */
    public void windowActivated(WindowEvent e) {
      Object o = e.getSource();
      if ( o instanceof javax.accessibility.Accessible ) {
        AccessibleContext ac = ((javax.accessibility.Accessible)o).getAccessibleContext();
        AtkWrapper.windowActivate(ac);
      }
    }

   /**
    * windowDeactivated:
    *                    Invoked when a Window is no longer the active Window.
    *                    Only a Frame or a Dialog can be the active Window. The
    *                    native windowing system may denote the active Window
    *                    or its children with special decorations, such as a
    *                    highlighted title bar. The active Window is always either
    *                    the focused Window, or the first Frame or Dialog that is
    *                    an owner of the focused Window.
    * @param e A WindowEvent object
    */
    public void windowDeactivated(WindowEvent e) {
      Object o = e.getSource();
      if (o instanceof javax.accessibility.Accessible) {
        AccessibleContext ac = ((javax.accessibility.Accessible)o).getAccessibleContext();
        AtkWrapper.windowDeactivate(ac);
      }
    }

   /**
    * windowStateChanged:
    *                     Invoked when a window state is changed.
    *
    * @param e A WindowEvent object
    */
    public void windowStateChanged(WindowEvent e) {
      Object o = e.getSource();
      if (o instanceof javax.accessibility.Accessible) {
        AccessibleContext ac = ((javax.accessibility.Accessible)o).getAccessibleContext();
        AtkWrapper.windowStateChange(ac);

        if( (e.getNewState() & Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH ) {
          AtkWrapper.windowMaximize(ac);
        }
      }
    }

   /**
    * windowDeiconified:
    *                   Invoked when a window is deiconified.
    * @param e A WindowEvent instance
    */
    public void windowDeiconified(WindowEvent e) {
      Object o = e.getSource();
      if (o instanceof javax.accessibility.Accessible) {
        AccessibleContext ac = ((javax.accessibility.Accessible)o).getAccessibleContext();
        AtkWrapper.windowRestore(ac);
      }
    }

   /**
    *  windowIconified:
    *                  Invoked when a window is iconified.
    * @param e A WindowEvent instance
    */
    public void windowIconified(WindowEvent e) {
      Object o = e.getSource();
      if (o instanceof javax.accessibility.Accessible) {
        AccessibleContext ac = ((javax.accessibility.Accessible)o).getAccessibleContext();
        AtkWrapper.windowMinimize(ac);
      }
    }

   /**
    *  windowOpened
    * @param e A WindowEvent object
    */
    public void windowOpened(WindowEvent e) {
      Object o = e.getSource();
      if ( o instanceof javax.accessibility.Accessible ) {
        boolean isToplevel = isToplevel(o);
        AccessibleContext ac = ((javax.accessibility.Accessible)o).getAccessibleContext();
        AtkWrapper.windowOpen(ac, isToplevel);
      }
    }

   /**
    * windowClosed:
    *              Invoked when a window has been closed.
    * @param e A WindowEvent object
    */
    public void windowClosed(WindowEvent e) {
      Object o = e.getSource();
      if (o instanceof javax.accessibility.Accessible) {
        boolean isToplevel = isToplevel(o);
        AccessibleContext ac = ((javax.accessibility.Accessible)o).getAccessibleContext();
        AtkWrapper.windowClose(ac, isToplevel);
      }
    }

   /**
    * windowClosing:
    *              Invoked when a window is in the process of being closed.
    * @param e A WindowEvent object
    */
    public void windowClosing(WindowEvent e) {
      Object o = e.getSource();
      if (o instanceof javax.accessibility.Accessible) {
        boolean isToplevel = isToplevel(o);
        AccessibleContext ac = ((javax.accessibility.Accessible)o).getAccessibleContext();
        AtkWrapper.windowClose(ac, isToplevel);
      }
    }

   /**
    * windowGainedFocus:
    *                   Invoked when the Window is set to be the focused Window,
    *                   which means that the Window, or one of its subcomponents,
    *                   will receive keyboard events.
    * @param e A WindowEvent object
    */
    public void windowGainedFocus(WindowEvent e) {
    }

    public void windowLostFocus(WindowEvent e) {
    }
  }; // Close WindowAdapter brace

 /**
  * isToplevel
  * @param o an instance
  * @return true if instance is of a window, frame or dialogue object
  *         false otherwise.
  */
  public static boolean isToplevel(Object o) {
    boolean isToplevel = false;
    if (o instanceof java.awt.Window ||
        o instanceof java.awt.Frame ||
        o instanceof java.awt.Dialog) {
      isToplevel = true;
    }
    return isToplevel;
  }

  final AWTEventListener globalListener = new AWTEventListener() {
    private boolean firstEvent = true;

    public void eventDispatched(AWTEvent e) {
      if (firstEvent && accessibilityEnabled) {
        firstEvent = false;
        try {
          AtkWrapper.loadAtkBridge();
        } catch (Exception ex) {
          ex.printStackTrace();
        }
      }

      if(e instanceof WindowEvent) {
        switch( e.getID() ) {
        case WindowEvent.WINDOW_OPENED:
          Window win = ((WindowEvent)e).getWindow();
          win.addWindowListener(winAdapter);
          win.addWindowStateListener(winAdapter);
          win.addWindowFocusListener(winAdapter);
          break;
        case WindowEvent.WINDOW_LOST_FOCUS:
          AtkWrapper.dispatchFocusEvent(null);
          break;
        default:
          break;
        }
      } else if(e instanceof ContainerEvent ) {
        switch( e.getID() ) {
          case ContainerEvent.COMPONENT_ADDED:
          {
            java.awt.Component c = ((ContainerEvent)e).getChild();
            if (c instanceof javax.accessibility.Accessible) {
              AccessibleContext ac = ((javax.accessibility.Accessible)c).getAccessibleContext();
              AtkWrapper.componentAdded(ac);
            }
            break;
          }
          case ContainerEvent.COMPONENT_REMOVED:
          {
            java.awt.Component c = ((ContainerEvent)e).getChild();
            if (c instanceof javax.accessibility.Accessible) {
              AccessibleContext ac = ((javax.accessibility.Accessible)c).getAccessibleContext();
              AtkWrapper.componentRemoved(ac);
            }
            break;
          }
        default:
          break;
        }
      } else if(e instanceof FocusEvent) {
        switch(e.getID()) {
        case FocusEvent.FOCUS_GAINED:
          AtkWrapper.dispatchFocusEvent(e.getSource());
          break;
        default:
          break;
        }
      }
    }
  };

  static AccessibleContext oldSourceContext = null;
  static AccessibleContext savedSourceContext= null;
  static AccessibleContext oldPaneContext = null;

 /**
  * dispatchFocusEvent
  * @param eventSource An instance of the event source object.
  */
  static void dispatchFocusEvent(Object eventSource) {
    if (eventSource == null) {
      oldSourceContext = null;
      return;
    }

    AccessibleContext ctx;

    try {
      if (eventSource instanceof AccessibleContext) {
        ctx = (AccessibleContext)eventSource;
      } else if (eventSource instanceof javax.accessibility.Accessible) {
        ctx = ((javax.accessibility.Accessible)eventSource).getAccessibleContext();
      } else {
        return;
      }

      if (ctx == oldSourceContext) {
        return;
      }

      if (oldSourceContext != null) {
        AccessibleRole role = oldSourceContext.getAccessibleRole();
        if (role == AccessibleRole.MENU || role == AccessibleRole.MENU_ITEM) {
          String jrootpane = "javax.swing.JRootPane$AccessibleJRootPane";
          String name = ctx.getClass().getName();

          if (jrootpane.compareTo(name) == 0) {
            oldPaneContext = ctx;
            return;
          }
        }
        savedSourceContext = ctx;
      } else if (oldPaneContext == ctx) {
      ctx = savedSourceContext;
      } else {
      savedSourceContext = ctx;
      }

      oldSourceContext = ctx;
      AccessibleRole role = ctx.getAccessibleRole();
      if (role == AccessibleRole.PAGE_TAB_LIST) {
        AccessibleSelection accSelection = ctx.getAccessibleSelection();

        if (accSelection != null && accSelection.getAccessibleSelectionCount() > 0) {
          Object child = accSelection.getAccessibleSelection(0);
          if (child instanceof AccessibleContext) {
            ctx = (AccessibleContext)child;
          } else if (child instanceof javax.accessibility.Accessible) {
            ctx = ((javax.accessibility.Accessible)child).getAccessibleContext();
          } else {
            return;
          }
        }
      }
      focusNotify(ctx);
    } catch (Exception e) {
        e.printStackTrace();
    }
  }

  final Toolkit toolkit = Toolkit.getDefaultToolkit();

  static PropertyChangeListener propertyChangeListener = new PropertyChangeListener() {

   /**
    * propertyChange:
    * @param e An instance of the PropertyChangeEvent object.
    */
    public void propertyChange(PropertyChangeEvent e) {
      Object o = e.getSource();
      AccessibleContext ac;
      if (o instanceof AccessibleContext) {
        ac = (AccessibleContext)o;
      } else if (o instanceof javax.accessibility.Accessible) {
        ac = ((javax.accessibility.Accessible)o).getAccessibleContext();
      } else {
        return;
      }

      Object oldValue = e.getOldValue();
      Object newValue = e.getNewValue();
      String propertyName = e.getPropertyName();
      if( propertyName.equals(AccessibleContext.ACCESSIBLE_CARET_PROPERTY) ) {
        Object[] args = new Object[1];
        args[0] = newValue;

        emitSignal(ac, AtkSignal.TEXT_CARET_MOVED, args);

      } else if( propertyName.equals(AccessibleContext.ACCESSIBLE_TEXT_PROPERTY) ) {
        if (newValue == null) {
          return;
        }

        if (newValue instanceof Integer) {
          Object[] args = new Object[1];
          args[0] = newValue;

          emitSignal(ac, AtkSignal.TEXT_PROPERTY_CHANGED, args);

        }
				/*
				if (oldValue == null && newValue != null) { //insertion event
					if (!(newValue instanceof AccessibleTextSequence)) {
						return;
					}

					AccessibleTextSequence newSeq = (AccessibleTextSequence)newValue;
					Object[] args = new Object[2];
					args[0] = new Integer(newSeq.startIndex);
					args[1] = new Integer(newSeq.endIndex - newSeq.startIndex);

					emitSignal(ac, AtkSignal.TEXT_PROPERTY_CHANGED_INSERT, args);

				} else if (oldValue != null && newValue == null) { //deletion event
					if (!(oldValue instanceof AccessibleTextSequence)) {
						return;
					}

					AccessibleTextSequence oldSeq = (AccessibleTextSequence)oldValue;
					Object[] args = new Object[2];
					args[0] = new Integer(oldSeq.startIndex);
					args[1] = new Integer(oldSeq.endIndex - oldSeq.startIndex);

					emitSignal(ac, AtkSignal.TEXT_PROPERTY_CHANGED_DELETE, args);

				} else if (oldValue != null && newValue != null) { //replacement event
					//It seems ATK does not support "replace" currently
					return;
				}*/
      } else if( propertyName.equals(AccessibleContext.ACCESSIBLE_CHILD_PROPERTY) ) {
        if (oldValue == null && newValue != null) { //child added
          AccessibleContext child_ac;
          if (newValue instanceof javax.accessibility.Accessible) {
            child_ac = ((javax.accessibility.Accessible)newValue).getAccessibleContext();
          } else {
            return;
          }

          Object[] args = new Object[2];
          args[0] = new Integer(child_ac.getAccessibleIndexInParent());
          args[1] = child_ac;

          emitSignal(ac, AtkSignal.OBJECT_CHILDREN_CHANGED_ADD, args);

        } else if (oldValue != null && newValue == null) { //child removed
          AccessibleContext child_ac;
          if (oldValue instanceof javax.accessibility.Accessible) {
            child_ac = ((javax.accessibility.Accessible)oldValue).getAccessibleContext();
          } else {
            return;
          }

          Object[] args = new Object[2];
          args[0] = new Integer(child_ac.getAccessibleIndexInParent());
          args[1] = child_ac;

          emitSignal(ac, AtkSignal.OBJECT_CHILDREN_CHANGED_REMOVE, args);

        }
      } else if( propertyName.equals(AccessibleContext.ACCESSIBLE_ACTIVE_DESCENDANT_PROPERTY) ) {
        AccessibleContext child_ac;
        if (newValue instanceof javax.accessibility.Accessible) {
          child_ac = ((javax.accessibility.Accessible)newValue).getAccessibleContext();
        } else {
          return;
        }

        Object[] args = new Object[1];
        args[0] = child_ac;

        emitSignal(ac, AtkSignal.OBJECT_ACTIVE_DESCENDANT_CHANGED, args);

        } else if( propertyName.equals(AccessibleContext.ACCESSIBLE_SELECTION_PROPERTY) ) {
          boolean isTextEvent = false;
          AccessibleRole role = ac.getAccessibleRole();
          if ((role == AccessibleRole.TEXT) ||
              role.toDisplayString(java.util.Locale.US).equalsIgnoreCase("paragraph")) {
            isTextEvent = true;
          } else if (role == AccessibleRole.MENU_BAR) {
            dispatchFocusEvent(o);
          } else if (role == AccessibleRole.PAGE_TAB_LIST) {
            AccessibleSelection selection = ac.getAccessibleSelection();
            if (selection != null &&
                selection.getAccessibleSelectionCount() > 0) {
              java.lang.Object child = selection.getAccessibleSelection(0);
              dispatchFocusEvent(child);
            }
          }

          if (!isTextEvent) {
            emitSignal(ac, AtkSignal.OBJECT_SELECTION_CHANGED, null);
          }

        } else if( propertyName.equals(AccessibleContext.ACCESSIBLE_VISIBLE_DATA_PROPERTY) ) {
          //emitSignal(ac, AtkSignal.OBJECT_VISIBLE_DATA_CHANGED, null);

        }else if( propertyName.equals(AccessibleContext.ACCESSIBLE_ACTION_PROPERTY) ) {
          Object[] args = new Object[2];
          args[0] = oldValue;
          args[1] = newValue;

          emitSignal(ac, AtkSignal.OBJECT_PROPERTY_CHANGE_ACCESSIBLE_ACTIONS, args);

        }else if( propertyName.equals(AccessibleContext.ACCESSIBLE_VALUE_PROPERTY) ) {
          if (oldValue instanceof Number && newValue instanceof Number) {
            Object[] args = new Object[2];
            args[0] = new Double(((Number)oldValue).doubleValue());
            args[1] = new Double(((Number)newValue).doubleValue());

            emitSignal(ac, AtkSignal.OBJECT_PROPERTY_CHANGE_ACCESSIBLE_VALUE, args);
          }

      } else if(propertyName.equals(AccessibleContext.ACCESSIBLE_DESCRIPTION_PROPERTY)) {
        emitSignal(ac, AtkSignal.OBJECT_PROPERTY_CHANGE_ACCESSIBLE_DESCRIPTION, null);

      } else if(propertyName.equals(AccessibleContext.ACCESSIBLE_NAME_PROPERTY)) {
        emitSignal(ac, AtkSignal.OBJECT_PROPERTY_CHANGE_ACCESSIBLE_NAME, null);

      } else if(propertyName.equals(AccessibleContext.ACCESSIBLE_HYPERTEXT_OFFSET)) {
        emitSignal(ac, AtkSignal.OBJECT_PROPERTY_CHANGE_ACCESSIBLE_HYPERTEXT_OFFSET, null);

      } else if(propertyName.equals(AccessibleContext.ACCESSIBLE_TABLE_MODEL_CHANGED)) {
        emitSignal(ac, AtkSignal.TABLE_MODEL_CHANGED, null);

      } else if( propertyName.equals(AccessibleContext.ACCESSIBLE_TABLE_CAPTION_CHANGED)) {
        emitSignal(ac, AtkSignal.OBJECT_PROPERTY_CHANGE_ACCESSIBLE_TABLE_CAPTION, null);

      } else if(propertyName.equals(AccessibleContext.ACCESSIBLE_TABLE_SUMMARY_CHANGED)) {
        emitSignal(ac, AtkSignal.OBJECT_PROPERTY_CHANGE_ACCESSIBLE_TABLE_SUMMARY, null);

      } else if(propertyName.equals(AccessibleContext.ACCESSIBLE_TABLE_COLUMN_HEADER_CHANGED)) {
        emitSignal(ac, AtkSignal.OBJECT_PROPERTY_CHANGE_ACCESSIBLE_TABLE_COLUMN_HEADER, null);

      } else if(propertyName.equals(AccessibleContext.ACCESSIBLE_TABLE_COLUMN_DESCRIPTION_CHANGED) ) {
        emitSignal(ac, AtkSignal.OBJECT_PROPERTY_CHANGE_ACCESSIBLE_TABLE_COLUMN_DESCRIPTION, null);

      } else if(propertyName.equals(AccessibleContext.ACCESSIBLE_TABLE_ROW_HEADER_CHANGED)) {
        emitSignal(ac, AtkSignal.OBJECT_PROPERTY_CHANGE_ACCESSIBLE_TABLE_ROW_HEADER, null);

      } else if(propertyName.equals(AccessibleContext.ACCESSIBLE_TABLE_ROW_DESCRIPTION_CHANGED)) {
        emitSignal(ac, AtkSignal.OBJECT_PROPERTY_CHANGE_ACCESSIBLE_TABLE_ROW_DESCRIPTION, null);

      } else if(propertyName.equals(AccessibleContext.ACCESSIBLE_STATE_PROPERTY)) {
        javax.accessibility.Accessible parent = ac.getAccessibleParent();
        javax.accessibility.AccessibleRole role = ac.getAccessibleRole();
        javax.accessibility.AccessibleRole parent_role = null;
        if (parent != null) {
          parent_role = parent.getAccessibleContext().getAccessibleRole();
        }
        if (role != null) {
          if (newValue == javax.accessibility.AccessibleState.FOCUSED ||
              newValue == javax.accessibility.AccessibleState.SELECTED) {
            dispatchFocusEvent(o);
          }
        }
        AccessibleState state;
        boolean value = false;
        if (newValue != null) {
          state = (AccessibleState)newValue;
          value = true;
        } else {
          state = (AccessibleState)oldValue;
          value = false;
        }

        if (state == AccessibleState.COLLAPSED) {
          state = AccessibleState.EXPANDED;
          value = false;
        }

        if(parent instanceof javax.swing.JComboBox && oldValue ==
           javax.accessibility.AccessibleState.VISIBLE) {
          objectStateChange(ac, AccessibleState.SHOWING, value);
        }

        objectStateChange(ac, state, value);

        if (parent instanceof javax.swing.JComboBox && newValue ==
            javax.accessibility.AccessibleState.VISIBLE && oldValue == null) {
          objectStateChange(ac, AccessibleState.SHOWING, value);
        }
      }
    }
  };

  public static void registerPropertyChangeListener(AccessibleContext ac) {
    if (ac != null) {
      try {
        ac.addPropertyChangeListener(propertyChangeListener);
      } catch (Exception e){
        e.printStackTrace();
      }
    }
  }

  public native static boolean initNativeLibrary();
  public native static void loadAtkBridge();

  public native static void focusNotify(javax.accessibility.AccessibleContext ac);

  public native static void windowOpen(javax.accessibility.AccessibleContext ac,
                                       boolean isToplevel);
  public native static void windowClose(javax.accessibility.AccessibleContext ac,
                                        boolean isToplevel);

  public native static void windowMinimize(javax.accessibility.AccessibleContext ac);
  public native static void windowMaximize(javax.accessibility.AccessibleContext ac);
  public native static void windowRestore(javax.accessibility.AccessibleContext ac);
  public native static void windowActivate(javax.accessibility.AccessibleContext ac);
  public native static void windowDeactivate(javax.accessibility.AccessibleContext ac);
  public native static void windowStateChange(javax.accessibility.AccessibleContext ac);

  public native static void emitSignal(javax.accessibility.AccessibleContext ac, int id, Object[] args);

  public native static void objectStateChange(javax.accessibility.AccessibleContext ac,
                                              java.lang.Object state, boolean value);

  public native static void componentAdded(javax.accessibility.AccessibleContext ac);
  public native static void componentRemoved(javax.accessibility.AccessibleContext ac);

  public native static boolean dispatchKeyEvent(AtkKeyEvent e);

  public static void printLog(String str) {
    System.out.println(str);
  }

  public AtkWrapper() {
    if (!accessibilityEnabled)
      return;

    toolkit.addAWTEventListener(globalListener,
                                AWTEvent.WINDOW_EVENT_MASK |
                                AWTEvent.FOCUS_EVENT_MASK |
                                AWTEvent.CONTAINER_EVENT_MASK);

    toolkit.getSystemEventQueue().push(new EventQueue() {
      boolean previousPressConsumed = false;

      public void dispatchEvent(AWTEvent e) {
        if (e instanceof KeyEvent) {
          if (e.getID() == KeyEvent.KEY_PRESSED) {
            boolean isConsumed = AtkWrapper.dispatchKeyEvent(new AtkKeyEvent((KeyEvent)e));
            if (isConsumed) {
              previousPressConsumed = true;
              return;
            }
          } else if (e.getID() == KeyEvent.KEY_TYPED) {
            if (previousPressConsumed) {
              return;
            }
          } else if (e.getID() == KeyEvent.KEY_RELEASED) {
            boolean isConsumed = AtkWrapper.dispatchKeyEvent(new AtkKeyEvent((KeyEvent)e));

            previousPressConsumed = false;
            if (isConsumed) {
              return;
            }
          }
        }

        super.dispatchEvent(e);
      }
    });
  }

  public static void main(String args[]) {
    new AtkWrapper();
  }
}