Jump to content
Sign in to follow this  

Menu accelerator auto tester

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 ****
#EndRegion ;**** Directives created by AutoIt3Wrapper_GUI ****
#include <guiconstantsex.au3>
#include "WinAPI.au3"
#Include <GuiMenu.au3>
#include <ListViewConstants.au3>
#include <WindowsConstants.au3>

Dim $ReportBuf = ""
Func Main()
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]
if StringLen($WinData) > 0 Then
  $WinData = StringRight($WinData, StringLen($WinData) - 1)
  GUICtrlSetData($lbWindows, $WinData)
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)
Local $nMsg
while 1
  $nMsg = GUIGetMsg()
  Switch $nMsg
   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)
; 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
Local $arMnuTxt[1] ; array to hold all menu texts found below
ReDim $arMnuTxt[$MenuEntryNum+1];$arMnuTxt[0]+1]
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 &= ">"
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
    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
    $AltKeysList &= StringUpper(StringMid($MnuTxt, $AmpOffset+1, 1))
   Local $hMenu = _GUICtrlMenu_GetItemSubMenu($hParentMenu, $iMnuEntry)
   CheckDupeMenuAccKeys($hMenu, 'Accelerator keys used in child menus of "' & $ParentMenuPath & $MnuTxt & '"', $ParentMenuPath & $MnuTxt, $bOfferSuggestions)
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
   $ReportBuf &= @CRLF & $Suggestions
; 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
Return True
; 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]
Local $arUnresolvedCharFreq[1]
ReDim $arUnresolvedCharFreq[$arMenuText[0]+1]
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 "&"
   $arWorkMenuText[$i] = StringReplace($arMenuText[$i], "&", "")
;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]
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))
   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
    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 & ")"
    ; 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))
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
       $CurChar = $FoundChar
    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)
Return "Suggestions: " & stringJoin($arWorkMenuText, @CRLF, 1, -1, False) & @CRLF & "Resulting Alt keys list" & @CRLF & $AltKeysList & "-----------"
; 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)
  if StringInStr($DupeList, ",")>0 Then
   $DupeInfoList &= "|" & $DupeList
if StringLen($DupeInfoList)>0 Then
  if StringLeft($DupeInfoList,1)="|" Then
   $DupeInfoList = StringRight($DupeInfoList,StringLen($DupeInfoList)-1)
; 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
   if Not $bSuggestionFound Then
    ; we don't have a suggestion that would work for everything, cancel existing suggestions
    Return ""
Return 'Suggestions:"' & $SuggestionRet & '"' & @CRLF & 'New accelerator key list:' & @CRLF & "'" & $AltKeysList & "'"
; 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]
Return ""
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
Return $FreqRet
; 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
if $IndexEnd<$IndexStart Then
  Return ""
Local $Ret
for $i = $IndexStart to $IndexEnd
  if (Not $bIncludeEmpties And StringLen($arIn[$i])>0) Or $bIncludeEmpties Then
   $Ret &= $Sep & $arIn[$i]
if StringLeft($Ret, StringLen($Sep)) = $Sep Then
  $Ret = StringRight($Ret, StringLen($Ret) - StringLen($Sep))
Return $Ret
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

Accelerator Key Menu Tester_1.0.zip

Edited by rodent1

Share this post

Link to post
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
Sign in to follow this  

  • Recently Browsing   0 members

    No registered users viewing this page.

  • Create New...