{*******************************************************}
{                                                       }
{       Tocsg.MSAA                                      }
{                                                       }
{       Copyright (C) 2022 kku                          }
{                                                       }
{*******************************************************}
unit Tocsg.MSAA;
interface
uses
  Winapi.Windows, Winapi.oleacc, System.Classes, System.SysUtils;

const
  IID_IServiceProvider: TGUID = '{6D5140C1-7436-11CE-8034-00AA006009FA}';
  IID_ISimpleDOMNode: TGUID = '{1814ceeb-49e2-407f-af99-fa755a7d2607}';//'{0C539790-12E4-11CF-B661-00AA004CD6D8}';
  IID_ISimpleDOMDocument: TGUID = '{0D68D6D0-D93D-4d08-A30D-F00DD1F45B24}';
  IID_IEnumVARIANT: TGUID ='{00020404-0000-0000-C000-000000000046}';

type
  IServiceProvider = interface(IUnknown)
    [IID_IServiceProvider]
    function QueryService(const rsid, IID: TGuid; out Obj): HResult; stdcall;
  end;

  ISimpleDOMNode = interface(IUnknown)
    [IID_ISimpleDOMNode]
    function QueryService(const rsid, IID: TGuid; out Obj): HResult; stdcall;
  end;

  ISimpleDOMDocument = interface(IUnknown)
    [IID_ISimpleDOMDocument]
    function QueryService(const rsid, IID: TGuid; out Obj): HResult; stdcall;
  end;

  TProcessEnumAccessible = reference to procedure(aParentAccObj, aAccObj: IAccessible; varChild: OleVariant; h: HWND; nLevel: Integer; var bContinue: Boolean);

  function GetObjectState(aAccObj: IAccessible; var aVarChild: OleVariant; var nState: Integer): Boolean;
  function GetObjectRole(aAccObj: IAccessible; var aVarChild: OleVariant; var nRole: Integer): Boolean;
  function GetObjectRoleString(aAccObj: IAccessible; var aVarChild: OleVariant; sData: PChar): Boolean;
  function GetObjectName(aAccObj: IAccessible; var aVarChild: OleVariant): String;
  function GetObjectValue(aAccObj: IAccessible; var aVarChild: OleVariant): String;
  function EnumAccessible(h: HWND; ProcEnumAccessible: TProcessEnumAccessible; bIncludeInvisible: Boolean = false): Boolean;
  function FindChildAccessible(aAccParent: IAccessible; ProcEnumAccessible: TProcessEnumAccessible; bFirst, bIncludeInvisible: Boolean): Boolean;

  function WindowFromAccessibleObject(aAccObj: IAccessible; var h: HWND): HRESULT; stdcall; external 'oleacc.dll';
  function AccessibleChildren(paccContainer : Pointer; iChildStart : LONGINT; cChildren : LONGINT;
                              out rgvarChildren : OleVariant; out pcObtained : LONGINT) : HRESULT; stdcall; external 'oleacc.dll';
  function GetRoleTextA(dwRole: DWORD; sRole: PAnsiChar; cRoleMax: byte):HRESULT; stdcall; external 'oleacc.dll';
  function GetRoleTextW(dwRole: DWORD; sRole: PWideChar; cRoleMax: byte):HRESULT; stdcall; external 'oleacc.dll';
  function GetRoleText(dwRole: DWORD; sRole: pchar; cRoleMax: byte):HRESULT; stdcall; external 'oleacc.dll' name 'GetRoleTextW';

implementation

uses
  Winapi.ActiveX, System.Variants, Tocsg.Exception,
  Tocsg.Trace;

var
  gnLevel: Integer = 0;

function GetObjectState(aAccObj: IAccessible; var aVarChild: OleVariant; var nState: Integer): Boolean;
var
  varState: OleVariant;
begin
  Result := false;
  try
    if (aAccObj <> nil) and not VarIsNull(aVarChild) then
    begin
      VariantInit(varState);
      try
        if SUCCEEDED(aAccObj.Get_accState(aVarChild, varState)) and (TVarData(varState).VType = VT_I4) then
        begin
          nState := TVarData(varState).VInteger;
          Result := true;
        end;
      finally
        VariantClear(varState);
      end;
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. GetObjectState()');
  end;
end;

function GetObjectRole(aAccObj: IAccessible; var aVarChild: OleVariant; var nRole: Integer): Boolean;
var
  varRole: OleVariant;
begin
  Result := false;
  try
    if (aAccObj <> nil) and not VarIsNull(aVarChild) then
    begin
      VariantInit(varRole);
      try
        if SUCCEEDED(aAccObj.Get_accRole(aVarChild, varRole)) and (TVarData(varRole).VType = VT_I4) then
        begin
          nRole := TVarData(varRole).VInteger;
          Result := true;
        end;
      finally
        VariantClear(varRole);
      end;
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. GetObjectRole()');
  end;
end;

//function GetObjectRoleString(aAccObj: IAccessible; var aVarChild: OleVariant): String;
function GetObjectRoleString(aAccObj: IAccessible; var aVarChild: OleVariant; sData: PChar): Boolean;
var
  varRole: OleVariant;
  nLen,
  nRole: Integer;
// 여기 안에서 메모리 생성하면 안되는듯.. 계속 크러쉬 난다 22_0623 14:12:59 kku
// arrBuf 선언하면 프로그램이 크러쉬 나고, pBuf로 동적으로 메모리 셋팅 하면 윈도우가 크러쉬 난다.
// 32bit 환경에서는 괜찮았던거 같은데...
//  pBuf: TBytes;
//  arrBuf: array [0..199] of WideChar; // 100에서 200으로 올림 22_0620 14:41:59 kku
begin
  Result := false;
  try
    if (aAccObj <> nil) and not VarIsNull(aVarChild) then
    begin
      VariantInit(varRole);
      try
        if SUCCEEDED(aAccObj.Get_accRole(aVarChild, varRole)) then // and
//           (TVarData(varRole).VType = VT_I4) then
        begin
          case TVarData(varRole).VType of
            VT_I4   :
            begin
              nRole := TVarData(varRole).VInteger;
//              nLen := GetRoleText(nRole, nil, 0);
//              if (nLen <= 0) or (nLen > 100) then
//                exit;
//              SetLength(pBuf, nLen * 2);
//              GetRoleText(nRole, @pBuf[0], nLen * 2);

//              ZeroMemory(@arrBuf, SizeOf(arrBuf));
//              nLen := GetRoleText(nRole, nil, 0);
//              if (nLen <= 0) or (nLen > 100) then
//                exit;
//              GetRoleText(nRole, @arrBuf, nLen * 2);
//
//              Result := arrBuf; // String(PChar(@pBuf[0]));

              nLen := GetRoleText(nRole, nil, 0);
              if (nLen <= 0) or (nLen > 100) then
              begin
              {$IFDEF DEBUG} ASSERT(false); {$ENDIF}
                exit;
              end;
              Result := GetRoleText(nRole, sData, nLen * 2) > 0;
            end;
//            VT_BSTR :
//            begin
//              Result := String(TVarData(varRole).VString);
//            end;
          end;
        end;
      finally
        VariantClear(varRole);
      end;
    end;
  except
    on E: Exception do
    begin
      ETgException.TraceException(E, 'Fail .. GetObjectRoleString()');
    end;
  end;
end;

function GetObjectName(aAccObj: IAccessible; var aVarChild: OleVariant): String;
var
  sVal: WideString;
begin
  try
    sVal := '';
    Result := '';
    if (aAccObj <> nil) and not VarIsNull(aVarChild) then
      if SUCCEEDED(aAccObj.Get_accName(aVarChild, sVal)) then
      begin
        Result := sVal;
      end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. GetObjectName()');
  end;
end;

function GetObjectValue(aAccObj: IAccessible; var aVarChild: OleVariant): String;
var
  sVal: WideString;
begin
  try
    sVal := '';
    Result := '';
    if (aAccObj <> nil) and not VarIsNull(aVarChild) then
      if SUCCEEDED(aAccObj.Get_accValue(aVarChild, sVal)) then
      begin
        Result := sVal;
      end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. GetObjectValue()');
  end;
end;

function EnumAccessible(h: HWND; ProcEnumAccessible: TProcessEnumAccessible; bIncludeInvisible: Boolean = false): Boolean;
var
  AccObj: IAccessible;
begin
  Result := false;
  try
    if IsWindow(h) and (@ProcEnumAccessible <> nil) then
    begin
      AccObj := nil;
      if SUCCEEDED(AccessibleObjectFromWindow(h, OBJID_WINDOW, IID_IAccessible, AccObj)) and (AccObj <> nil) then
        FindChildAccessible(AccObj, ProcEnumAccessible, true, bIncludeInvisible);
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. EnumAccessible()');
  end;
end;

//function FindChildAccessible(aAccParent: IAccessible; ProcEnumAccessible: TProcessEnumAccessible; bFirst, bIncludeInvisible: Boolean): Boolean;
//var
//  enumVar: IEnumVARIANT;
//  varChild: OleVariant;
//  AccChild: IAccessible;
//  bContinue: Boolean;
//  i, nChildCnt, nCnt,
//  nState, nRole: Integer;
//  dwFetched: DWORD;
//  Dispatch: IDispatch;
//  hChild: HWND;
//  arrChild: array of OleVariant;
//begin
//  Result := false;
//  try
//    bContinue := true;
//    enumVar := nil;
//
//    if bFirst then
//    begin
//      gnLevel := 0;
//      VariantInit(varChild);
//      TVarData(varChild).VType := VT_I4;
//      TVarData(varChild).VInteger := CHILDID_SELF;
//      ProcEnumAccessible(aAccParent, varChild, 0, gnLevel, bContinue);
//    end;
//    Inc(gnLevel);
//
////    aAccParent.QueryInterface(IID_IEnumVARIANT, enumVar);
////    if enumVar <> nil then
////      enumVar.Reset;
//
//    nChildCnt := 0;
//    aAccParent.Get_accChildCount(nChildCnt);
//
//    if nChildCnt = 0 then
//      exit;
//
//    nCnt := 0;
//    if AccessibleChildren(aAccParent, 0, nChildCnt, arrChild[0], nCnt) <> S_OK then
//      exit;
//
//    SetLength(arrChild, nChildCnt);
//    for i := 0 to nCnt - 1 do
//    begin
//      if not bContinue then
//        break;
//
////      VariantClear(varChild);
//      varChild := arrChild[i];
//
//      AccChild := nil;
////      if enumVar <> nil then
////      begin
////        dwFetched := 0;
////        if not SUCCEEDED(enumVar.Next(1, varChild, dwFetched)) then
////        begin
////          Result := false;
////          exit;
////        end;
////
////      // 위에서 성공해도 varChild가 "Unassigned" 상태일 수 있다.
////      // 이럴땐 기본값 강제로 넣어주도록 보완 18_0718 17:17:00 kku
////        if VarIsEmpty(varChild) then
////        begin
////          TVarData(varChild).VType := VT_I4;
////          TVarData(varChild).VInteger := i;
////        end;
////      end else begin
////        TVarData(varChild).VType := VT_I4;
////        TVarData(varChild).VInteger := i;
////      end;
//
//      Dispatch := nil;
//      case TVarData(varChild).VType of
//        VT_I4       : aAccParent.Get_accChild(varChild, Dispatch);
//        VT_DISPATCH : Dispatch := IDispatch(TVarData(varChild).VDispatch);
//      end;
//
//      if Dispatch <> nil then
//      begin
//        Dispatch.QueryInterface(IID_IAccessible, AccChild);
//
//        if AccChild <> nil then
//        begin
//          VariantInit(varChild);
//          TVarData(varChild).VType := VT_I4;
//          TVarData(varChild).VInteger := CHILDID_SELF;
//        end;
//      end;
//
//      nState := 0;
//      hChild := 0;
//      if AccChild <> nil then
//      begin
//  //      nRole := 0;
//  //      GetObjectRole(AccChild, varChild, nRole);
//
//        GetObjectState(AccChild, varChild, nState);
//        if not bIncludeInvisible and ((nState and STATE_SYSTEM_INVISIBLE) <> 0) then
//          continue;
//
//        WindowFromAccessibleObject(AccChild, hChild);
////      {$IFDEF DEBUG}
////        if gnLevel = 5 then
////          gnLevel := gnLevel + 0;
////      {$ENDIF}
//        ProcEnumAccessible(AccChild, varChild, hChild, gnLevel, bContinue);
//
//        if bContinue then
//          bContinue := FindChildAccessible(AccChild, ProcEnumAccessible, false, bIncludeInvisible);
//      end else begin
//        GetObjectState(aAccParent, varChild, nState);
//        if not bIncludeInvisible and ((nState and STATE_SYSTEM_INVISIBLE) <> 0) then
//          continue;
//
//        WindowFromAccessibleObject(aAccParent, hChild);
//        ProcEnumAccessible(aAccParent, varChild, hChild, gnLevel, bContinue);
//      end;
//    end;
//    VariantClear(varChild);
//    Dec(gnLevel);
//    Result := bContinue;
//  except
//    on E: Exception do
//      ETgException.TraceException(E, 'Fail .. FindChildAccessible()');
//  end;
//end;

function FindChildAccessible(aAccParent: IAccessible; ProcEnumAccessible: TProcessEnumAccessible; bFirst, bIncludeInvisible: Boolean): Boolean;
var
  enumVar: IEnumVARIANT;
  varChild: OleVariant;
  AccChild: IAccessible;
  bContinue: Boolean;
  i, nChildCnt,
  nState, nRole: Integer;
  dwFetched: DWORD;
  Dispatch: IDispatch;
  hChild: HWND;
  arrChild: array of OleVariant;
begin
  Result := false;
  try
    bContinue := true;
    enumVar := nil;

    if bFirst then
    begin
      gnLevel := 0;
      VariantInit(varChild);
      TVarData(varChild).VType := VT_I4;
      TVarData(varChild).VInteger := CHILDID_SELF;
      ProcEnumAccessible(nil, aAccParent, varChild, 0, gnLevel, bContinue);
    end;
    Inc(gnLevel);

    aAccParent.QueryInterface(IID_IEnumVARIANT, enumVar);
    if enumVar <> nil then
      enumVar.Reset;

    nChildCnt := 0;
    aAccParent.Get_accChildCount(nChildCnt);
    for i := 1 {0=self} to nChildCnt do
    begin
      if not bContinue then
        break;

      VariantClear(varChild);

      AccChild := nil;
      if enumVar <> nil then
      begin
        dwFetched := 0;
        if not SUCCEEDED(enumVar.Next(1, varChild, dwFetched)) then
        begin
          Result := false;
          exit;
        end;

      // 위에서 성공해도 varChild가 "Unassigned" 상태일 수 있다.
      // 이럴땐 기본값 강제로 넣어주도록 보완 18_0718 17:17:00 kku
        if VarIsEmpty(varChild) then
        begin
          TVarData(varChild).VType := VT_I4;
          TVarData(varChild).VInteger := i;
        end;
      end else begin
        TVarData(varChild).VType := VT_I4;
        TVarData(varChild).VInteger := i;
      end;

      Dispatch := nil;
      case TVarData(varChild).VType of
        VT_I4       : aAccParent.Get_accChild(varChild, Dispatch);
        VT_DISPATCH : Dispatch := IDispatch(TVarData(varChild).VDispatch);
      end;

      if Dispatch <> nil then
      begin
        Dispatch.QueryInterface(IID_IAccessible, AccChild);

        if AccChild <> nil then
        begin
          VariantInit(varChild);
          TVarData(varChild).VType := VT_I4;
          TVarData(varChild).VInteger := CHILDID_SELF;
        end;
      end;

      nState := 0;
      hChild := 0;
      if AccChild <> nil then
      begin
  //      nRole := 0;
  //      GetObjectRole(AccChild, varChild, nRole);

        GetObjectState(AccChild, varChild, nState);
        if not bIncludeInvisible and ((nState and STATE_SYSTEM_INVISIBLE) <> 0) then
          continue;

        WindowFromAccessibleObject(AccChild, hChild);
//      {$IFDEF DEBUG}
//        if gnLevel = 5 then
//          gnLevel := gnLevel + 0;
//      {$ENDIF}
        ProcEnumAccessible(aAccParent, AccChild, varChild, hChild, gnLevel, bContinue);

        if bContinue then
          bContinue := FindChildAccessible(AccChild, ProcEnumAccessible, false, bIncludeInvisible);
      end else begin
        GetObjectState(aAccParent, varChild, nState);
        if not bIncludeInvisible and ((nState and STATE_SYSTEM_INVISIBLE) <> 0) then
          continue;

        WindowFromAccessibleObject(aAccParent, hChild);
        ProcEnumAccessible(nil, aAccParent, varChild, hChild, gnLevel, bContinue);
      end;
    end;
    VariantClear(varChild);
    Dec(gnLevel);
    Result := bContinue;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. FindChildAccessible()');
  end;
end;

(*
// 나중에 필요하면 포팅해서 쓰자 14_1112 14:40:03 kku 
//=============================================================================
BOOL CXMSAALib::GetObjectDescription(IAccessible *pAcc,
									 VARIANT *pvarChild,
									 LPTSTR lpszDescription,
									 UINT cchDescription)
//=============================================================================
{
	BOOL bRet = FALSE;
	_ASSERTE(pAcc);
	_ASSERTE(pvarChild);
	_ASSERTE(lpszDescription);
	if (pAcc && pvarChild && lpszDescription)
	{
		lpszDescription[0] = 0;
		BSTR bstrDescription = NULL;
		HRESULT hr = pAcc->get_accDescription(*pvarChild, &bstrDescription);
		if (SUCCEEDED(hr) && bstrDescription)
		{
			_bstr_t btemp(bstrDescription, false);
			if ((LPCTSTR)btemp)
				_tcsncpy(lpszDescription, btemp, cchDescription);
			bRet = TRUE;
		}
	}
	return bRet;
}
//=============================================================================
BOOL CXMSAALib::GetObjectLocation(IAccessible *pAcc,
								  VARIANT *pvarChild,
								  RECT& rect)
//=============================================================================
{
	BOOL bRet = FALSE;
	_ASSERTE(pAcc);
	_ASSERTE(pvarChild);
	if (pAcc && pvarChild)
	{
		HRESULT hr = pAcc->accLocation(&rect.left, &rect.top,
						&rect.right, &rect.bottom, *pvarChild);
		if (SUCCEEDED(hr))
		{
			// accLocation returns width and height
			rect.right += rect.left;
			rect.bottom += rect.top;
			bRet = TRUE;
		}
	}
	return bRet;
}
BOOL CXMSAALib::GetObjectChildCount(IAccessible *pAcc,
									long& nCount)
//=============================================================================
{
	BOOL bRet = FALSE;
	_ASSERTE(pAcc);
	if (pAcc)
	{
		HRESULT hr = pAcc->get_accChildCount(&nCount);
		if (SUCCEEDED(hr))
		{
			bRet = TRUE;
		}
	}
	return bRet;
}
BOOL CXMSAALib::GetObjectStateString(IAccessible *pAcc,
									 VARIANT *pvarChild,
									 LPTSTR lpszBuf,
									 UINT cchBuf)
//=============================================================================
{
	BOOL bRet = FALSE;
	_ASSERTE(pAcc);
	_ASSERTE(pvarChild);
	_ASSERTE(lpszBuf);
	if (pAcc && pvarChild && lpszBuf)
	{
		lpszBuf[0] = 0;
		UINT nState = 0;
		if (GetObjectState(pAcc, pvarChild, nState))
		{
			if ((nState & STATE_SYSTEM_INVISIBLE) == 0)
			{
				if (cchBuf > _tcslen(_T("visible")))
					_tcscpy(lpszBuf, _T("visible"));
			}
			TCHAR szState[200];
			szState[0] = 0;
			UINT nChars = 0;
			// convert state flags to comma separated list
			for (DWORD dwStateBit = STATE_SYSTEM_UNAVAILABLE;
				 (dwStateBit < STATE_SYSTEM_HASPOPUP) && (nChars < cchBuf);
				 dwStateBit <<= 1)
			{
				if (nState & dwStateBit)
				{
					szState[0] = 0;
					nChars += GetStateText(dwStateBit, szState, sizeof(szState)/sizeof(TCHAR)-1);
					if ((nChars < (cchBuf-3)) && (szState[0] != 0))
					{
						if (lpszBuf[0] != 0)
							_tcscat(lpszBuf, _T(","));
						_tcscat(lpszBuf, szState);
						nChars = (UINT)_tcslen(lpszBuf);
					}
				}
			}
			bRet = TRUE;
		}
	}
	return bRet;
}
typedef void (CALLBACK *XWINEVENTURLPROC)(IAccessible *pAccChild,
										  VARIANT *pvarChild,
										  DWORD event,
										  HWND hwnd);
typedef BOOL (CALLBACK *XENUMACCESSIBLEPROC)(IAccessible *pAccChild,
											 VARIANT *pvarChild,
											 HWND hwndChild,
											 int nLevel,
											 LPARAM lParam);
static XWINEVENTURLPROC g_lpfnXWinEventUrlProc = 0;
//=============================================================================
static void CALLBACK CXMSAALib_WinEventProc(HWINEVENTHOOK /*hook*/,
											DWORD event,
											HWND hwnd,
											LONG idObject,
											LONG idChild,
											DWORD /*dwEventThread*/,
											DWORD /*dwmsEventTime*/)
//=============================================================================
{
	switch (event)
	{
		case EVENT_OBJECT_FOCUS:
		{
			TRACE(_T("event=0x%X\n"), event);
			IAccessible *pIAcc = 0;
			VARIANT varChild;
			VariantInit(&varChild);
			HRESULT hr = AccessibleObjectFromEvent(hwnd, idObject, idChild,
							&pIAcc, &varChild);
			if ((hr == S_OK) && (pIAcc != NULL))
			{
				if (IsWindow(hwnd))
				{
					TRACE(_T("hwnd=0x%X\n"), hwnd);
					TCHAR szClassname[256];
					::GetClassName(hwnd, szClassname, sizeof(szClassname)/sizeof(TCHAR)-1);
					szClassname[sizeof(szClassname)/sizeof(TCHAR)-1] = 0;
					TRACE(_T("class %s\n"), szClassname);
				}
				if (g_lpfnXWinEventUrlProc)
				{
					g_lpfnXWinEventUrlProc(pIAcc, &varChild, event, hwnd);
					TRACE(_T("after event proc\n"));
				}
				SAFE_RELEASE(pIAcc);
			}
			break;
		}
		default:
			break;
	}
}
//=============================================================================
BOOL CXMSAALib::StartGetObjectFromEvent(XWINEVENTURLPROC lpfnXWinEventUrlProc)
//=============================================================================
{
	TRACE(_T("in CXMSAALib::StartGetObjectFromEvent\n"));
	g_lpfnXWinEventUrlProc = 0;
	if (lpfnXWinEventUrlProc)
	{
		g_lpfnXWinEventUrlProc = lpfnXWinEventUrlProc;
	}
	_ASSERTE(g_lpfnXWinEventUrlProc);
	if (g_lpfnXWinEventUrlProc == 0)
	{
		StopGetObjectFromEvent();
	}
	else if (g_lpfnXWinEventUrlProc)
	{
		if (!m_eHook)
		{
			m_eHook = ::SetWinEventHook(EVENT_OBJECT_FOCUS, EVENT_OBJECT_VALUECHANGE, 0,
				CXMSAALib_WinEventProc, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
		}
	}
	TRACE(_T("exiting CXMSAALib::StartGetObjectFromEvent\n"));
	return m_eHook != 0;
}
//=============================================================================
void CXMSAALib::StopGetObjectFromEvent()
//=============================================================================
{
	if (m_eHook)
		::UnhookWinEvent(m_eHook);
	m_eHook = 0;
	g_lpfnXWinEventUrlProc = 0;
}
*)
end.
