unit VA508AccessibilityRouter;

interface

uses
  SysUtils, Windows, Registry, StrUtils, Classes, Controls, Dialogs,
  Contnrs, DateUtils, Forms, ExtCtrls;

type
  TComponentDataNeededEvent = procedure(const WindowHandle: HWND; var DataStatus: LongInt;
    var Caption: PChar; var Value: PChar; var Data: PChar; var ControlType: PChar;
    var State: PChar; var Instructions: PChar; var ItemInstructions: PChar) of object;

  TKeyMapProcedure = procedure;

  TVA508ScreenReader = class(TObject)
  protected
    procedure RegisterCustomClassBehavior(Before, After: string); virtual; abstract;
    procedure RegisterClassAsMSAA(ClassName: string); virtual; abstract;
    procedure AddComponentDataNeededEventHandler(event: TComponentDataNeededEvent); virtual; abstract;
    procedure RemoveComponentDataNeededEventHandler(event: TComponentDataNeededEvent); virtual; abstract;
  public
    procedure Speak(Text: string); virtual; abstract;
    procedure RegisterDictionaryChange(Before, After: string); virtual; abstract;
    procedure RegisterCustomKeyMapping(Key: string; proc: TKeyMapProcedure;
                              shortDescription, longDescription: string); virtual; abstract;
  end;

function GetScreenReader: TVA508ScreenReader;

{ TODO -oJeremy Merrill -c508 : 
if ScreenReaderSystemActive is false, but there are valid DLLs, add a recheck every 30 seconds
to see if the screen reader is running.  in the timer event, see if DLL.IsRunning is running is true.
if it is then pop up a message to the user (only once) and inform them that if they restart the app
with the screen reader running it will work better.  After the popup disable the timer event. }
function ScreenReaderSystemActive: boolean;

// Only guaranteed to be valid if called in an initialization section
// all other components stored as .dfm files will be registered as a dialog
// using the RegisterCustomClassBehavior
procedure SpecifyFormIsNotADialog(FormClass: TClass);

// do not call this routine - called by screen reader DLL
procedure ComponentDataRequested(WindowHandle: HWND; DataRequest: LongInt); stdcall;

implementation

uses VAUtils, VA508ScreenReaderDLLLinker, VAClasses, VA508AccessibilityConst;

type
  TNullScreenReader = class(TVA508ScreenReader)
  public
    procedure Speak(Text: string); override;
    procedure RegisterDictionaryChange(Before, After: string); override;
    procedure RegisterCustomClassBehavior(Before, After: string); override;
    procedure RegisterClassAsMSAA(ClassName: string); override;
    procedure RegisterCustomKeyMapping(Key: string; proc: TKeyMapProcedure;
                              shortDescription, longDescription: string); override;
    procedure AddComponentDataNeededEventHandler(event: TComponentDataNeededEvent); override;
    procedure RemoveComponentDataNeededEventHandler(event: TComponentDataNeededEvent); override; 
  end;

  TMasterScreenReader = class(TVA508ScreenReader)
  strict private
    FEventHandlers: TVAMethodList;
    FCustomBehaviors: TStringList;
    FInternalRegistration: boolean;
    FDataHasBeenRegistered: boolean;
    FTrying2Register: boolean;
    FKeyProc: TList;
  private
    function EncodeBehavior(Before, After: string; Action: integer): string;
    procedure DecodeBehavior(code: string; var Before, After: string;
      var Action: integer);
    function RegistrationAllowed: boolean;
    procedure RegisterCustomData;
  protected
    procedure RegisterCustomBehavior(Str1, Str2: String; Action: integer; CheckIR: boolean = FALSE);
    procedure ProcessCustomKeyCommand(DataRequest: integer);
    property EventHandlers: TVAMethodList read FEventHandlers;
  public
    constructor Create;
    destructor Destroy; override;
    procedure HandleSRException(E: Exception);
    procedure Speak(Text: string); override;
    procedure RegisterDictionaryChange(Before, After: string); override;
    procedure RegisterCustomClassBehavior(Before, After: string); override;
    procedure RegisterClassAsMSAA(ClassName: string); override;
    procedure RegisterCustomKeyMapping(Key: string; proc: TKeyMapProcedure;
                              shortDescription, longDescription: string); override;
    procedure AddComponentDataNeededEventHandler(event: TComponentDataNeededEvent); override;
    procedure RemoveComponentDataNeededEventHandler(event: TComponentDataNeededEvent); override;
  end;

var
  ActiveScreenReader: TVA508ScreenReader = nil;
  MasterScreenReader: TMasterScreenReader = nil;
  uNonDialogClassNames: TStringList = nil;
  SaveInitProc: Pointer = nil;
  Need2RegisterData: boolean = FALSE;
  OK2RegisterData: boolean = FALSE;
  CheckScreenReaderSystemActive: boolean = TRUE;
  uScreenReaderSystemActive: boolean = FALSE;
  uPostScreenReaderActivationTimer: TTimer = nil;

const
// number of seconds between checks for a screen reader
  POST_SCREEN_READER_ACTIVATION_CHECK_SECONDS = 30;

  POST_SCREEN_READER_INFO_MESSAGE = ERROR_INTRO +
    'The Accessibility Framework can only communicate with the screen' + CRLF +
    'reader if the screen reader is running before you start this application.'+ CRLF +
    'Please restart %s to take advantage of the enhanced'+ CRLF +
    'accessibility features offered by the Accessibility Framework.';

procedure VA508RouterInitProc;
begin
  if assigned(SaveInitProc) then
    TProcedure(SaveInitProc);
  OK2RegisterData := TRUE;
  if Need2RegisterData then
  begin
    Need2RegisterData := FALSE;
    if ScreenReaderSystemActive then
    begin
      TMasterScreenReader(GetScreenreader).RegisterCustomData;
    end;
  end;
end;

function GetScreenReader: TVA508ScreenReader;
begin
  if not assigned(ActiveScreenReader) then
  begin
    if ScreenReaderSystemActive then
    begin
      MasterScreenReader := TMasterScreenReader.Create;
      ActiveScreenReader := MasterScreenReader;
    end
    else
      ActiveScreenReader := TNullScreenReader.Create;
  end;
  Result := ActiveScreenReader;
end;

procedure PostScreenReaderCheckEvent(Self: TObject; Sender: TObject);
var
  AppName, ext, error: string;
begin
  if ScreenReaderActive then
  begin
    FreeAndNil(uPostScreenReaderActivationTimer);
    if IsScreenReaderSupported(TRUE) then
    begin
      AppName := ExtractFileName(ParamStr(0));
      ext := ExtractFileExt(AppName);
      AppName := LeftStr(AppName, length(AppName) - Length(ext));
      error := Format(POST_SCREEN_READER_INFO_MESSAGE, [AppName]);
      MessageBox(0, PChar(error), 'Accessibility Component Information',
        MB_OK or MB_ICONINFORMATION or MB_TASKMODAL or MB_TOPMOST);
    end;
  end;
end;

function ScreenReaderSystemActive: boolean;

  procedure CreateTimer;
  var
    ptr: TMethod;
  begin
    uPostScreenReaderActivationTimer := TTimer.Create(nil);
    with uPostScreenReaderActivationTimer do
    begin
      Enabled := FALSE;
      Interval := 1000 * POST_SCREEN_READER_ACTIVATION_CHECK_SECONDS;
      ptr.Code := @PostScreenReaderCheckEvent;
      ptr.Data := @ptr;
      OnTimer := TNotifyEvent(ptr);
      Enabled := TRUE;
    end;
  end;

begin
  if CheckScreenReaderSystemActive then
  begin
    CheckScreenReaderSystemActive := FALSE;
    // prevent Delphi IDE from running DLL 
    if LowerCase(ExtractFileName(ParamStr(0))) <> 'bds.exe' then
      uScreenReaderSystemActive := ScreenReaderDLLsExist;
    if uScreenReaderSystemActive then
    begin
      if ScreenReaderSupportEnabled then
      begin
        if IsScreenReaderSupported(FALSE) then
          uScreenReaderSystemActive := InitializeScreenReaderLink
        else
          uScreenReaderSystemActive := FALSE;
      end
      else
      begin
        uScreenReaderSystemActive := FALSE;
        CreateTimer;
      end;
    end;
  end;
  Result := uScreenReaderSystemActive;
end;

procedure SpecifyFormIsNotADialog(FormClass: TClass);
var
  lc: string;
begin
  if ScreenReaderSystemActive then
  begin
    lc := lowercase(FormClass.ClassName);
    if not assigned(uNonDialogClassNames) then
      uNonDialogClassNames := TStringList.Create;
    if uNonDialogClassNames.IndexOf(lc) < 0 then
      uNonDialogClassNames.Add(lc);
    if assigned(MasterScreenReader) then
      MasterScreenReader.RegisterCustomBehavior(FormClass.ClassName, '',
                                        BEHAVIOR_REMOVE_COMPONENT_CLASS, TRUE);
  end;
end;

{ TMasterScreenReader }

procedure TMasterScreenReader.AddComponentDataNeededEventHandler(event: TComponentDataNeededEvent);
begin
  FEventHandlers.Add(TMethod(event));
end;

constructor TMasterScreenReader.Create;
begin
  FEventHandlers := TVAMethodList.Create;
  FCustomBehaviors := TStringList.Create;
  FInternalRegistration := FALSE;
  FDataHasBeenRegistered := FALSE;
  FKeyProc := TList.Create;
end;

procedure TMasterScreenReader.DecodeBehavior(code: string; var Before,
  After: string; var Action: integer);

  function Decode(var MasterString: string): string;
  var
    CodeLength: integer;
    hex: string;

  begin
    Result := '';
    if length(MasterString) > 1 then
    begin
      hex := copy(MasterString,1,2);
      CodeLength := FastHexToByte(hex);
      Result := copy(MasterString, 3, CodeLength);
      delete(MasterString, 1, CodeLength + 2);
    end;
  end;

begin
  Action := StrToIntDef(Decode(code), 0);
  Before := Decode(code);
  After := Decode(code);
  if code <> '' then
    Raise TVA508Exception.Create('Corrupted Custom Behavior');
end;

destructor TMasterScreenReader.Destroy;
begin
  CloseScreenReaderLink;
  FreeAndNil(FEventHandlers);
  FreeAndNil(FCustomBehaviors);
  FreeAndNil(FKeyProc);
  inherited;
end;

function TMasterScreenReader.EncodeBehavior(Before, After: string;
  Action: integer): string;

  function Coded(str: string): string;
  var
    len: integer;
  begin
    len := length(str);
    if len > 255 then
      Raise TVA508Exception.Create('RegisterCustomBehavior parameter can not be more than 255 characters long');
    Result := HexChars[len] + str;
  end;

begin
  Result := Coded(IntToStr(Action)) + Coded(Before) + Coded(After);
end;

procedure TMasterScreenReader.HandleSRException(E: Exception);
begin
  if not E.ClassNameIs(TVA508Exception.ClassName) then
    raise E;
end;

procedure TMasterScreenReader.ProcessCustomKeyCommand(DataRequest: integer);
var
  idx: integer;
  proc: TKeyMapProcedure;
begin
  idx := (DataRequest AND DATA_CUSTOM_KEY_COMMAND_MASK) - 1;
  if (idx < 0) or (idx >= FKeyProc.count) then exit;
  proc := TKeyMapProcedure(FKeyProc[idx]);
  proc;
end;

procedure TMasterScreenReader.RegisterClassAsMSAA(ClassName: string);
begin
  RegisterCustomBehavior(ClassName, '', BEHAVIOR_ADD_COMPONENT_MSAA, TRUE);
  RegisterCustomBehavior(ClassName, '', BEHAVIOR_REMOVE_COMPONENT_CLASS, TRUE);
end;

procedure TMasterScreenReader.RegisterCustomBehavior(Str1, Str2: String;
          Action: integer; CheckIR: boolean = FALSE);
var
  code: string;
  idx: integer;
  p2: PChar;
  ok: boolean;
begin
  code := EncodeBehavior(Str1, Str2, Action);
  idx := FCustomBehaviors.IndexOf(code);
  if idx < 0 then
  begin
    FCustomBehaviors.add(code);
    ok := RegistrationAllowed;
    if ok and CheckIR then
      ok := (not FInternalRegistration);
    if ok then
    begin
      try
        if Str2 = '' then
          p2 := nil
        else
          p2 := PChar(Str2);
        SRRegisterCustomBehavior(Action, PChar(Str1), P2);
      except
        on E: Exception do HandleSRException(E);
      end;
    end;
  end;
end;

procedure TMasterScreenReader.RegisterCustomClassBehavior(Before,
  After: string);
begin
  RegisterCustomBehavior(Before, After, BEHAVIOR_ADD_COMPONENT_CLASS, TRUE);
  RegisterCustomBehavior(Before, After, BEHAVIOR_REMOVE_COMPONENT_MSAA, TRUE);
end;

function EnumResNameProc(module: HMODULE; lpszType: PChar; lpszName: PChar; var list: TStringList): BOOL; stdcall;
var
  name: string;

begin
  name := lpszName;
  list.Add(name);
  Result := TRUE;
end;

procedure TMasterScreenReader.RegisterCustomData;
var
  i, action: integer;
  before, after, code: string;

  procedure EnsureDialogAreSpecified;
  var
    list: TStringList;
    i: integer;
    stream: TResourceStream;
    Reader: TReader;
    ChildPos: Integer;
    Flags: TFilerFlags;
    clsName: string;
    ok: boolean;
  begin
    FInternalRegistration := TRUE;
    try
      list := TStringList.Create;
      try
        if EnumResourceNames(HInstance, RT_RCDATA, @EnumResNameProc, integer(@list)) then
        begin
          for i := 0 to list.Count-1 do
          begin
            stream := TResourceStream.Create(HInstance, list[i], RT_RCDATA);
            try
              Reader := TReader.Create(stream, 512);
              try
                try
                  reader.ReadSignature;
                  reader.ReadPrefix(Flags, ChildPos);
                  clsName := reader.ReadStr;
                  ok := not assigned(uNonDialogClassNames);
                  if not ok then
                    ok := (uNonDialogClassNames.IndexOf(lowercase(clsName)) < 0);
                  if ok then                  
                    RegisterCustomClassBehavior(clsName, CLASS_BEHAVIOR_DIALOG);
                except
                end;
              finally
                Reader.Free;
              end;
            finally
              stream.Free;
            end;
          end;
        end;
      finally
        list.free;
      end;
    finally
      FInternalRegistration := FALSE;
    end;
  end;

begin
  if FTrying2Register then exit;
  FTrying2Register := TRUE;
  try
    if OK2RegisterData then
    begin
      try
        EnsureDialogAreSpecified;
        RegisterCustomBehavior('','',BEHAVIOR_PURGE_UNREGISTERED_KEY_MAPPINGS);
        for i := 0 to FCustomBehaviors.Count-1 do
        begin
          code := FCustomBehaviors[i];
          DecodeBehavior(code, before, after, action);
          SRRegisterCustomBehavior(Action, PChar(Before), PChar(After));
        end;
        FDataHasBeenRegistered := TRUE;
      except
        on E: Exception do HandleSRException(E);
      end;
    end
    else
      Need2RegisterData := TRUE;
  finally
    FTrying2Register := FALSE;
  end;
end;

procedure TMasterScreenReader.RegisterCustomKeyMapping(Key: string; proc: TKeyMapProcedure;
                              shortDescription, longDescription: string);
var
  idx: string;

  procedure AddDescription(DescType, Desc: string);
  var
    temp: string;
  begin
    temp := DescType + idx + '=' + Desc;
    if length(temp) > 255 then
      raise TVA508Exception.Create('Key Mapping description for ' + Key + ' exceeds 255 characters');
    RegisterCustomBehavior(DescType + idx, Desc, BEHAVIOR_ADD_CUSTOM_KEY_DESCRIPTION);
  end;

begin
  FKeyProc.Add(@proc);
  idx := inttostr(FKeyProc.Count);
  RegisterCustomBehavior(Key, idx, BEHAVIOR_ADD_CUSTOM_KEY_MAPPING);
  AddDescription('short', shortDescription);
  AddDescription('long', longDescription);
end;

procedure TMasterScreenReader.RegisterDictionaryChange(Before, After: string);
begin
  RegisterCustomBehavior(Before, After, BEHAVIOR_ADD_DICTIONARY_CHANGE);
end;

function TMasterScreenReader.RegistrationAllowed: boolean;
begin
  Result := FDataHasBeenRegistered;
  if not Result then
  begin
    RegisterCustomData;
    Result := FDataHasBeenRegistered;
  end;
end;

procedure TMasterScreenReader.RemoveComponentDataNeededEventHandler(event: TComponentDataNeededEvent);
begin
  FEventHandlers.Remove(TMethod(event));
end;

procedure TMasterScreenReader.Speak(Text: string);
begin
  if (not assigned(SRSpeakText)) or (Text = '') then exit;
  try
    SRSpeakText(PChar(Text));
  except
    on E: Exception do HandleSRException(E);
  end;
end;

// need to post a message here - can't do direct call - this message is called before mouse
// process messages are called that change a check box state
procedure ComponentDataRequested(WindowHandle: HWND; DataRequest: LongInt); stdcall;
var
  i: integer;
  Handle: HWND;
  Caption: PChar;
  Value: PChar;
  Data: PChar;
  ControlType: PChar;
  State: PChar;
  Instructions: PChar;
  ItemInstructions: PChar;
  DataStatus: LongInt;

  handler: TComponentDataNeededEvent;

begin
  if assigned(MasterScreenReader) then
  begin
    try
      if (DataRequest AND DATA_CUSTOM_KEY_COMMAND) <> 0 then
        MasterScreenReader.ProcessCustomKeyCommand(DataRequest)
      else
      begin
        Handle := WindowHandle;
        Caption := nil;
        Value := nil;
        Data := nil;
        ControlType := nil;
        State := nil;
        Instructions := nil;
        ItemInstructions := nil;
        DataStatus := DataRequest;
        i := 0;
        while (i < MasterScreenReader.EventHandlers.Count) do
        begin
          handler := TComponentDataNeededEvent(MasterScreenReader.EventHandlers.Methods[i]);
          if assigned(handler) then
            handler(Handle, DataStatus, Caption, Value, Data, ControlType, State,
                      Instructions, ItemInstructions);
          inc(i);
        end;
        SRComponentData(WindowHandle, DataStatus, Caption, Value, Data, ControlType, State, Instructions, ItemInstructions);
      end;
    except
      on E: Exception do MasterScreenReader.HandleSRException(E);
    end;
  end;
end;

{ TNullScreenReader }

procedure TNullScreenReader.AddComponentDataNeededEventHandler(
  event: TComponentDataNeededEvent);
begin
end;

procedure TNullScreenReader.RegisterClassAsMSAA(ClassName: string);
begin
end;

procedure TNullScreenReader.RegisterCustomClassBehavior(Before, After: string);
begin
end;

procedure TNullScreenReader.RegisterCustomKeyMapping(Key: string; proc: TKeyMapProcedure;
                              shortDescription, longDescription: string);
begin

end;

procedure TNullScreenReader.RegisterDictionaryChange(Before, After: string);
begin
end;

procedure TNullScreenReader.RemoveComponentDataNeededEventHandler(
  event: TComponentDataNeededEvent);
begin
end;

procedure TNullScreenReader.Speak(Text: string);
begin
end;

initialization
  SaveInitProc := InitProc;
  InitProc := @VA508RouterInitProc;

finalization
  if assigned(ActiveScreenReader) then
    FreeAndNil(ActiveScreenReader);
  if assigned(uNonDialogClassNames) then
    FreeAndNil(uNonDialogClassNames);
  if assigned(uPostScreenReaderActivationTimer) then
    FreeAndNil(uPostScreenReaderActivationTimer);    

end.
