Jump to content

Menu accelerator auto tester


rodent1
 Share

Recommended Posts

When translation companies send back their translated strings, accelerator keys (alt keys to call a control or menu from the keyboard, marked by an "&" before the key, which Windows displays as an underlined character) typically come back all messed up, with duplicate alt keys everywhere. Going through all menus of an app to fix that can be tedious. So here is a script that will detect all duplicates, and if "offer suggestions" is checked, it will also do its best to come up with a working solution.The script displays the desktop-child visible windows in a listbox. Select the window you want to check, and click on the button below. The results are displayed in a message box, use ctrl-C to copy its contents so you can paste it where it's convenient.The algorithm makes a first pass looking for easy fixes, looping through all characters in each menu entry with a duplicate alt key till a character if found that is not in the list of already-used keys. If that fails for at least one menu entry, suggestions are generated that rely on less frequently used characters to try patterns. It should work most of the time, as long as the menus are not too huge.If you come up with ways that will always work, yet not take too long, please share your improvements! Enjoy...

The code:

#Region ;**** Directives created by AutoIt3Wrapper_GUI ****
#AutoIt3Wrapper_UseX64=n
#AutoIt3Wrapper_Res_Fileversion=1.0
#AutoIt3Wrapper_Res_Fileversion_AutoIncrement=p
#AutoIt3Wrapper_Res_Language=1033
#EndRegion ;**** Directives created by AutoIt3Wrapper_GUI ****
#include <guiconstantsex.au3>
#include "WinAPI.au3"
#Include <GuiMenu.au3>
#include <ListViewConstants.au3>
#include <WindowsConstants.au3>

Dim $ReportBuf = ""
Main()
Func Main()
ShowWinList()
EndFunc
Func ShowWinList()
Local $frmMain = GUICreate("Accelerator Key Menu Tester", 623, 450, 192, 124)
Local $lbWindows = GUICtrlCreateList("", 10, 10, 600, 400)
Local $arWinData = WinList()
Local $WinData
for $i = 1 to $arWinData[0][0]
  if _WinAPI_IsWindowVisible($arWinData[$i][1]) Then
   Local $sWinTitle = $arWinData[$i][0]
   if StringLen($sWinTitle) > 0 Then
    if BitAND(WinGetState($arWinData[$i][1]),16)=0 Then
     $WinData &= "|" & $sWinTitle & ";" & $arWinData[$i][1] & ";" & $arWinData[$i][0]
    EndIf
   EndIf
  EndIf
Next
if StringLen($WinData) > 0 Then
  $WinData = StringRight($WinData, StringLen($WinData) - 1)
  GUICtrlSetData($lbWindows, $WinData)
EndIf
Local $btnTestMnuAcKeyDupes = GUICtrlCreateButton("Test Duplicate Menu Accelerator Keys", 10, 420, 200, 25)
GUICtrlSetState(-1, $GUI_DISABLE)
Local $chkOfferSuggestions = GUICtrlCreateCheckbox("Offer Suggestions", 220, 420, 200, 25)
GUICtrlSetState(-1, $GUI_DISABLE)
GUISetState(@SW_SHOW)
Local $nMsg
while 1
  $nMsg = GUIGetMsg()
  Switch $nMsg
   Case $GUI_EVENT_CLOSE
    Exit
   case $lbWindows ; should fire when a listbox selection is made
    GUICtrlSetState($btnTestMnuAcKeyDupes, $GUI_ENABLE) ; enable the button
    GUICtrlSetState($chkOfferSuggestions, $GUI_ENABLE) ; enable the checkbox
   case $btnTestMnuAcKeyDupes
    Local $hwnd = GetSplitToken(GUICtrlRead($lbWindows), 2, ";")
    TestMenu($hwnd, GUICtrlRead($chkOfferSuggestions) = $GUI_CHECKED)
    MsgBox(262144,'Duplicate Accelerator Key Info',"Test Window Title = '" & GetSplitToken(GUICtrlRead($lbWindows), 1, ";") & "'" & $ReportBuf)
  EndSwitch
WEnd
EndFunc
; check the accelerator keys for duplicates in all child menu entries of parent menu
;   $hMainMenu = handle of parent menu
;   $ReportString = string that will precede the list of found accelerator keys
;   $ParentMenuPath = '>'-separated menu access path
Func CheckDupeMenuAccKeys($hParentMenu, $ReportString, $ParentMenuPath, $bOfferSuggestions)
Local $MenuEntryNum = _GUICtrlMenu_GetItemCount($hParentMenu)
if $MenuEntryNum <= 0 Then
  Return
EndIf
Local $arMnuTxt[1] ; array to hold all menu texts found below
ReDim $arMnuTxt[$MenuEntryNum+1];$arMnuTxt[0]+1]
$arMnuTxt[0]=$MenuEntryNum
Local $AltKeysList = "" ; list of found accelerator keys
Local $iAltKeyIndex = 0 ; index into the array $arMnuTxt
if StringLen($ParentMenuPath)>0 Then ; append a separator if the parent menu text(s) is provided
  $ParentMenuPath &= ">"
EndIf
Local $bFoundDuplicate = False
for $iMnuEntry = 0 to $MenuEntryNum
  Local $MnuTxt = _GUICtrlMenu_GetItemText($hParentMenu,$iMnuEntry)
  if StringLen($MnuTxt)>0 Then
   Local $AmpOffset = StringInStr($MnuTxt, "&")
   if $AmpOffset>0 Then
    $iAltKeyIndex += 1
    $arMnuTxt[$iAltKeyIndex]=$MnuTxt
    Local $AltChrOffset = StringInStr($AltKeysList, StringUpper(StringMid($MnuTxt, $AmpOffset+1, 1)))
    if $AltChrOffset>0 Then
     $ReportBuf &= @CRLF & 'Duplicate menu accelerator key ' & StringMid($MnuTxt, $AmpOffset+1, 1) & ', ' & $ParentMenuPath & $MnuTxt & " conflicts with " & $ParentMenuPath & $arMnuTxt[$AltChrOffset] & " (menu entry #" & $iMnuEntry & ")"
     $bFoundDuplicate = True
    EndIf
    $AltKeysList &= StringUpper(StringMid($MnuTxt, $AmpOffset+1, 1))
   EndIf
   Local $hMenu = _GUICtrlMenu_GetItemSubMenu($hParentMenu, $iMnuEntry)
   CheckDupeMenuAccKeys($hMenu, 'Accelerator keys used in child menus of "' & $ParentMenuPath & $MnuTxt & '"', $ParentMenuPath & $MnuTxt, $bOfferSuggestions)
  EndIf
Next
if StringLen($AltKeysList)>0 Then
  $ReportBuf &= @CRLF & $ReportString & "=" & $AltKeysList
  if $bFoundDuplicate And $bOfferSuggestions Then
   Local $Suggestions = GenerateSimpleSuggestions($AltKeysList, $arMnuTxt)
   if StringLen($Suggestions)=0 Then ; basic suggestion generation failed
    $Suggestions = GenerateSuggestions($arMnuTxt) ; strip existing alt keys, and restart from scratch
   EndIf
   $ReportBuf &= @CRLF & $Suggestions
  EndIf
EndIf
EndFunc
; a character is acceptable for an alt key if it's not a space, and it's not a foreign character (accented like ü, or like ç, ß) that not all keyboards could use.
func IsAcceptable($Character)
if $Character=" " Or (asc($Character)>127 And $Character<254) then
  Return False
EndIf
Return True
EndFunc
; Called when a duplicate menu accelerator is found and Generate Simple Suggestions was not completely successful
; Purpose: generate a suggestion for a unique accelerator for each entry with a duplicate accelerator
;         Strip all &'s from all menu entries, if there is a "&&", replace it with "~~" as a place marker, then
;         Make a list of chars found in list of menu entries with no "&" and their frequencies, then
;         loop through list of chars with frequency 1 and place an & in front of them in menu texts that don't have one yet, then
;         make a list of chars found in list of menu entries with no "&" and their frequencies, skipping the ones that are already attributed, then
;         loop through all remaining list of chars with frequency 1 and place an & in front of them in menu texts that don't have one yet, then
;         repeat in a loop till there is no change compared to previous loop or till all menu entries have an "&", then
;         if not all menu entries have an "&", repeat with lowest frequency instead of looking for a frequency of 1.
;
;    $arMenuText  = array of menu texts found in the current menu scope
;    returns: string containing suggestion information
Func GenerateSuggestions($arMenuText)
; 1- Strip all '&'s from all menu entries
Local $arWorkMenuText[1]
ReDim $arWorkMenuText[$arMenuText[0]+1]
$arWorkMenuText[0]=$arMenuText[0]
Local $arUnresolvedCharFreq[1]
ReDim $arUnresolvedCharFreq[$arMenuText[0]+1]
$arUnresolvedCharFreq[0]=$arMenuText[0]
Local $AltKeysList = ""
Local $bSolutionIncomplete = False
for $i = 1 to $arMenuText[0]
  if StringInStr($arMenuText[$i], "&&")>0 Then
   $arWorkMenuText[$i] = StringReplace($arMenuText[$i], "&&", "~~") ; place marker "~~", to be replaced at the end with a "&"
  Else
   $arWorkMenuText[$i] = StringReplace($arMenuText[$i], "&", "")
  EndIf
Next
;2- Make a list of chars found in list of menu entries with no "&" and their frequencies
Local $arCharList[1]
redim $arCharList[$arWorkMenuText[0]+1]
$arCharList[0]=$arWorkMenuText[0]
for $iMnuEntry = 1 to $arWorkMenuText[0]
  if StringLen($arWorkMenuText[$iMnuEntry])>0 Then
   Local $FoundChars = ""
   for $iChar = 1 to StringLen($arWorkMenuText[$iMnuEntry])
    if StringInStr($FoundChars, StringMid($arWorkMenuText[$iMnuEntry], $iChar, 1))=0 Then
     if IsAcceptable(StringMid($arWorkMenuText[$iMnuEntry], $iChar, 1)) Then
      $FoundChars &= StringUpper(StringMid($arWorkMenuText[$iMnuEntry], $iChar, 1))
     EndIf
    EndIf
   Next
   Local $AllUnprocessedMnuTxt = stringJoin($arWorkMenuText, "|~", $iMnuEntry+1, -1, False) ;join of all $arMenuText starting with next $iMnuEntry
   if StringLen($AllUnprocessedMnuTxt)>0 Then
    ; find first character in $FoundChars that is not found in $AllUnprocessedMnuTxt, if any
    Local $bResolved = False
    for $iChar = 1 to StringLen($FoundChars)
     if StringInStr($AllUnprocessedMnuTxt, StringMid($FoundChars, $iChar, 1))=0 Then
      Local $CharLoc = StringInStr(StringUpper($arWorkMenuText[$iMnuEntry]), StringMid($FoundChars, $iChar, 1))
      $arWorkMenuText[$iMnuEntry] = StringLeft($arWorkMenuText[$iMnuEntry], $CharLoc-1) & "&" & StringRight($arWorkMenuText[$iMnuEntry], StringLen($arWorkMenuText[$iMnuEntry]) - $CharLoc + 1)
      $AltKeysList &= StringUpper(StringMid($FoundChars, $iChar, 1))
      $bResolved = True
      ExitLoop
     EndIf
    Next
    if Not $bResolved Then
     $bSolutionIncomplete = True
     for $iChar = 1 to StringLen($FoundChars)
      Local $CharFreq = StringGetSubstrFreq($arWorkMenuText, $iMnuEntry, StringMid($FoundChars, $iChar, 1))
      $arUnresolvedCharFreq[$iMnuEntry] &= StringMid($FoundChars, $iChar, 1) & "(" & $CharFreq & ")"
     Next
    EndIf
   Else
    ; this would have to be the last menu item in scope
    for $i=1 to StringLen($arWorkMenuText[$iMnuEntry])
     if StringInStr($AltKeysList, StringUpper(StringMid($arWorkMenuText[$iMnuEntry], $i, 1)))=0 Then
      $arWorkMenuText[$iMnuEntry] = StringLeft($arWorkMenuText[$iMnuEntry], $i-1) & "&" & StringRight($arWorkMenuText[$iMnuEntry], StringLen($arWorkMenuText[$iMnuEntry]) - $i + 1)
      $AltKeysList &= StringUpper(StringMid($arWorkMenuText[$iMnuEntry], $i, 1))
      ExitLoop
     EndIf
    Next
   EndIf
  EndIf
Next
if $bSolutionIncomplete Then
  ; for each unresolved menu entry find char with lowest frequency not in $AltKeysList, use it, add it to $AltKeysList
  for $iMnuEntry=1 to $arWorkMenuText[0]
   if StringLen($arUnresolvedCharFreq[$iMnuEntry])>0 Then
    Local $arTokens = StringSplit($arUnresolvedCharFreq[$iMnuEntry], ")")
    Local $MinFreq=999
    Local $CurChar=""
    for $iToken = 1 to $arTokens[0]
     Local $CurFreq = StringRight($arTokens[$iToken], StringLen($arTokens[$iToken])-StringInStr($arTokens[$iToken], "("))
     if $CurFreq*1<$MinFreq And $CurFreq*1>0 Then
      Local $FoundChar = StringLeft($arTokens[$iToken], 1)
      if StringInStr($AltKeysList, $FoundChar)=0 Then
       $MinFreq=$CurFreq*1
       $CurChar = $FoundChar
      EndIf
     EndIf
    Next
    if StringLen($FoundChar)>0 Then
     Local $CharLoc = StringInStr(StringUpper($arWorkMenuText[$iMnuEntry]), $CurChar)
     $arWorkMenuText[$iMnuEntry] = StringLeft($arWorkMenuText[$iMnuEntry], $CharLoc-1) & "&" & StringRight($arWorkMenuText[$iMnuEntry], StringLen($arWorkMenuText[$iMnuEntry]) - $CharLoc + 1)
     $AltKeysList &= StringUpper($CurChar)
    EndIf
   EndIf
  Next
EndIf
Return "Suggestions: " & stringJoin($arWorkMenuText, @CRLF, 1, -1, False) & @CRLF & "Resulting Alt keys list" & @CRLF & $AltKeysList & "-----------"
EndFunc
; Called when a duplicate menu accelerator is found
; Purpose: generate a suggestion for a unique accelerator for each entry with a duplicate accelerator
;         only a simple algorythm is implemented:
;         find all menu entries with a duplicate accelerator key, then
;         loop through all characters in each such menu entry till a character if found that is not in the list of used accelerator keys, then
;         place the "&" in front of the found unique character, then
;         if not all menus have a suggestion, offer no suggestion
;    $AltKeysList = list of accelerator keys found in the current menu scope
;    $arMenuText  = array of menu texts found in the current menu scope
;    returns: string containing suggestion information
Func GenerateSimpleSuggestions($AltKeysList, $arMenuText)
Local $SuggestionRet
; $AltKeysList is a list of the accelerator keys. Record the duplicate indices in a pipe-separated list of comma-separated duplicate indices
Local $DupeInfoList = ""
for $iOut=1 to StringLen($AltKeysList)-1
  Local $DupeList = $iOut
  for $iIn = $iOut+1 to StringLen($AltKeysList)
   if StringMid($AltKeysList, $iOut, 1) = StringMid($AltKeysList, $iIn, 1) And StringMid($AltKeysList, $iOut, 1)<>" " Then
    $DupeList &= "," & $iIn
    $AltKeysList = StringLeft($AltKeysList, $iIn-1) & " " & StringRight($AltKeysList, StringLen($AltKeysList)-$iIn)
   EndIf
  Next
  if StringInStr($DupeList, ",")>0 Then
   $DupeInfoList &= "|" & $DupeList
  EndIf
Next
if StringLen($DupeInfoList)>0 Then
  if StringLeft($DupeInfoList,1)="|" Then
   $DupeInfoList = StringRight($DupeInfoList,StringLen($DupeInfoList)-1)
  EndIf
EndIf
; starting with the first duplicate (2nd instance of use of same accelerator key), look for a character that's not in use
for  $iKey = 1 to StringLen($AltKeysList)
  if StringMid($AltKeysList, $iKey, 1) = " " Then
   Local $bSuggestionFound = False
   ; look at each character in menu entry, find the first one that's not already in use, if any
   for $iChar = 1 to StringLen($arMenuText[$iKey])-1
    Local $MenuChar = StringMid($arMenuText[$iKey], $iChar, 1)
    if IsAcceptable($MenuChar) And $MenuChar <> "&" Then
     if StringInStr($AltKeysList, StringUpper($MenuChar))=0 Then
      $arMenuText[$iKey] = StringReplace($arMenuText[$iKey], "&", "")
      Local $KeyLoc = StringInStr($arMenuText[$iKey], $MenuChar)
      $SuggestionRet &= @CRLF & '"' & StringLeft($arMenuText[$iKey], $KeyLoc-1) & "&" & StringRight($arMenuText[$iKey], StringLen($arMenuText[$iKey]) - $KeyLoc + 1)
      $AltKeysList = StringLeft($AltKeysList, $iKey-1) & StringUpper($MenuChar) & StringRight($AltKeysList, StringLen($AltKeysList) - $iKey)
      $bSuggestionFound = True
      ExitLoop
     EndIf
    EndIf
   Next
   if Not $bSuggestionFound Then
    ; we don't have a suggestion that would work for everything, cancel existing suggestions
    Return ""
   EndIf
  EndIf
Next
Return 'Suggestions:"' & $SuggestionRet & '"' & @CRLF & 'New accelerator key list:' & @CRLF & "'" & $AltKeysList & "'"
EndFunc
; replicate the vbscript syntax split(string, separator)(index)
; to return the indexth token of the array resulting from the split command
Func GetSplitToken($sIn, $iNdx, $sSep)
if StringLen($sIn) > 0 Then
  if StringInStr($sIn, $sSep) > 0 Then
   Local $arTokens = StringSplit($sIn, $sSep)
   if $arTokens[0] >= $iNdx Then
    Return $arTokens[$iNdx]
   EndIf
  EndIf
EndIf
Return ""
EndFunc
Func StringGetSubstrFreq($arText, $iSkipEntry, $CurChar)
Local $FreqRet = 0
for $i = 1 to $arText[0]
  if StringInStr($arText[$i], "&")=0 Then ; if the menu entry & location is not yet resolved
   if $i <> $iSkipEntry Then ; if this is not the current menu entry
    if StringInStr(StringUpper($arText[$i]), $CurChar)>0 Then
     $FreqRet += 1
    EndIf
   EndIf
  EndIf
Next
Return $FreqRet
EndFunc
; replicate the vbscript syntax of Join function, strArrayItemList = join(ItemArray, strSeparator)
Func stringJoin($arIn, $Sep = "|", $IndexStart = 0, $IndexEnd = -1, $bIncludeEmpties = True)
if $IndexEnd = -1 Then
  $IndexEnd = UBound($arIn) - 1
EndIf
if $IndexEnd<$IndexStart Then
  Return ""
EndIf
Local $Ret
for $i = $IndexStart to $IndexEnd
  if (Not $bIncludeEmpties And StringLen($arIn[$i])>0) Or $bIncludeEmpties Then
   $Ret &= $Sep & $arIn[$i]
  EndIf
Next
if StringLeft($Ret, StringLen($Sep)) = $Sep Then
  $Ret = StringRight($Ret, StringLen($Ret) - StringLen($Sep))
EndIf
Return $Ret
EndFunc
Func TestMenu($hwndParentWin, $bOfferSuggestions)
Local $hMainMenu = _GUICtrlMenu_GetMenu($hwndParentWin)
CheckDupeMenuAccKeys($hMainMenu, "Accelerator keys used in main menu header", "", $bOfferSuggestions)
if StringInStr($ReportBuf, "Duplicate menu accelerator")=0 Then
  $ReportBuf = "No duplicate accelerator keys were found!" & @CRLF & @CRLF & $ReportBuf
EndIf
EndFunc

Accelerator Key Menu Tester_1.0.zip

Edited by rodent1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...