AHK v2: 4 mods to enable two-way compatibility
Posted: 06 Nov 2019, 20:57
4 EDITS TO AHK V2: SCRIPTS THAT WORK IN BOTH AHK V1/V2
I made 4 edits to the AHK v2 source code, that allowed AHK v2 to run both AHK v1/v2 code.
(Note: where the AHK v1 code is written in a forwards compatible way and uses custom backport functions.)
This is also mentioned here:
conversion logic, v1 = -> v1 := -> v2, two-way compatibility - Page 7 - AutoHotkey Community
https://autohotkey.com/boards/viewtopic.php?f=37&t=27069&p=280143#p280143
EDIT 1: NUMERIC/STRING COMPARISON
One unintuitive change was made to numeric/string comparison in AHK v2. The main dev has suggested it ought to be changed back.
My conclusion for a simple/effective set of rules is: if two items look numeric, compare numerically, otherwise alphabetically. Use StrCompare (and perhaps introduce StrEquals) to force an alphabetic comparison.
See also:
numbers/strings in expressions - AutoHotkey Community
https://autohotkey.com/boards/viewtopic.php?f=37&t=55905
EDIT 2: CONTROLXXX FUNCTIONS: ALLOW OMITTING 'CONTROL' PARAMETER
The Control parameter in most ControlXXX functions was made mandatory.
I found the previous behaviour logical and intuitive: check the WinXXX parameters, then the Control parameter (if present) to determine which window/control window receives the message.
Also, maintaining the previous behaviour greatly eases script conversion, cf. manual checking/editing.
Furthermore, some ControlXXX functions still allow 'Control' to be omitted. Allowing 'Control' to be omitted for all ControlXXX functions gives consistency, and a memorable rule.
EDIT 3: ALLOW #IFWINACTIVE (UNTIL #IF WINACTIVE() IS OPTIMISED IN AHK V1)
#If WinActive("ahk_class Notepad") etc, is doable in AHK v1, but it is slow.
So until/unless #If WinActive() is optimised in AHK v1, it is better to continue using #IfWinActive.
The edit allows you to run AHK v1 scripts without changing all of the #IfWinActive lines.
EDIT 4: ALLOW '% ' IN LOOPS (UNTIL A TWO-WAY COMPATIBLE AHK V1/V2 LOOP IS INTRODUCED)
The biggest obstacle to two-way compatible code is as follows:
So until a two-way compatible LoopXXX syntax is introduced to AHK v1/v2, allowing forced expressions in AHK v2, solely for the Loop control flow statement, is the best temporary solution.
The edit allows the '% ' to remain in AHK v2.
CODE AND NOTES
I made 4 edits to the AHK v2 source code, that allowed AHK v2 to run both AHK v1/v2 code.
(Note: where the AHK v1 code is written in a forwards compatible way and uses custom backport functions.)
This is also mentioned here:
conversion logic, v1 = -> v1 := -> v2, two-way compatibility - Page 7 - AutoHotkey Community
https://autohotkey.com/boards/viewtopic.php?f=37&t=27069&p=280143#p280143
EDIT 1: NUMERIC/STRING COMPARISON
One unintuitive change was made to numeric/string comparison in AHK v2. The main dev has suggested it ought to be changed back.
Code: Select all
var1 := "1"
var2 := "1.0"
MsgBox(var1 = var2) ;0 (AHK v2 at present) ;1 (AHK v2 when fix is applied, AHK v1)
See also:
numbers/strings in expressions - AutoHotkey Community
https://autohotkey.com/boards/viewtopic.php?f=37&t=55905
EDIT 2: CONTROLXXX FUNCTIONS: ALLOW OMITTING 'CONTROL' PARAMETER
The Control parameter in most ControlXXX functions was made mandatory.
I found the previous behaviour logical and intuitive: check the WinXXX parameters, then the Control parameter (if present) to determine which window/control window receives the message.
Also, maintaining the previous behaviour greatly eases script conversion, cf. manual checking/editing.
Furthermore, some ControlXXX functions still allow 'Control' to be omitted. Allowing 'Control' to be omitted for all ControlXXX functions gives consistency, and a memorable rule.
Code: Select all
hCtl := ControlGetHwnd("Edit1", "ahk_class Notepad")
vText := ControlGetText(, "ahk_id " hCtl) ;no longer allowed
vText := ControlGetText(hCtl)
EDIT 3: ALLOW #IFWINACTIVE (UNTIL #IF WINACTIVE() IS OPTIMISED IN AHK V1)
#If WinActive("ahk_class Notepad") etc, is doable in AHK v1, but it is slow.
So until/unless #If WinActive() is optimised in AHK v1, it is better to continue using #IfWinActive.
The edit allows you to run AHK v1 scripts without changing all of the #IfWinActive lines.
Code: Select all
#IfWinActive ahk_class Notepad ;AHK v1 only
#If WinActive("ahk_class Notepad") ;AHK v1/v2 ;preferable
EDIT 4: ALLOW '% ' IN LOOPS (UNTIL A TWO-WAY COMPATIBLE AHK V1/V2 LOOP IS INTRODUCED)
The biggest obstacle to two-way compatible code is as follows:
Code: Select all
Loop Parse, vText, % "`n", % "`r" ;AHK v1 only
Loop Parse, vText, "`n", "`r" ;AHK v2 only
LoopParse vText, "`n", "`r" ;AHK v1/v2 my proposal
The edit allows the '% ' to remain in AHK v2.
CODE AND NOTES
Code: Select all
AHK v2 4 mods (tested on AutoHotkey v2.0-a106)
note: compiled with VS 2017 (compiling did not work with VS 2013)
[download source code]
Releases · Lexikos/AutoHotkey_L · GitHub
https://github.com/Lexikos/AutoHotkey_L/releases
[tips re. compiling]
AutoHotkey C++ Powerhouse: Introduction - AutoHotkey Community
https://autohotkey.com/boards/viewtopic.php?f=75&t=54394
[notes re. installing Visual Studio/compiling AutoHotkey_H]
[Guide] Compiling Ahk_h's source step by step - AutoHotkey Community
https://autohotkey.com/boards/viewtopic.php?f=65&t=62308
[summary of 4 modifications]
conversion logic, v1 = -> v1 := -> v2, two-way compatibility - Page 7 - AutoHotkey Community
https://autohotkey.com/boards/viewtopic.php?f=37&t=27069&p=280143#p280143
[all 4 modifications in 1 example]
#IfWinActive ahk_class Notepad
;#If WinActive("ahk_class Notepad")
q::
hCtl := ControlGetHwnd("Edit1", "ahk_class Notepad")
vText := ControlGetText(, "ahk_id " hCtl)
;vText := ControlGetText(hCtl)
MsgBox(vText)
Loop Parse, vText, % "`n", % "`r"
;Loop Parse, vText, "`n", "`r"
vCount := A_Index
MsgBox(vCount)
var1 := "1"
var2 := "1.0"
MsgBox(var1 = var2) ;0 ;1 when fix is applied
MsgBox(0 + var1 = 0 + var2) ;1
#If
==================================================
[if (a = b): restore the numeric comparison of string variables][redundant if the behaviour is restored in AHK v2]
Revised handling of types (integer/float/string). · Lexikos/AutoHotkey_L@275216e · GitHub
https://github.com/Lexikos/AutoHotkey_L/commit/275216e20724568ba7afbe6f2cc29297fd162be2
note: this doesn't restore all of the AHK v1 string/numeric comparison behaviour
compare these numerically again:
var1 := "1"
var2 := "1.0"
MsgBox(var1 = var2) ;0 ;1 when fix is applied
MsgBox(0 + var1 = 0 + var2) ;1
[script_expression.cpp]
[BEFORE]
if ( !(right_is_number && left_is_number) // i.e. they're not both numeric (or this is SYM_CONCAT).
|| IS_RELATIONAL_OPERATOR(this_token.symbol) && !right_is_pure_number && !left_is_pure_number ) // i.e. if both are strings, compare them alphabetically.
{
[BEFORE END]
[AFTER]
if ( !(right_is_number && left_is_number) // i.e. they're not both numeric (or this is SYM_CONCAT).
//==============================
//jeeswg edit: code modified
// || IS_RELATIONAL_OPERATOR(this_token.symbol) && !right_is_pure_number && !left_is_pure_number ) // i.e. if both are strings, compare them alphabetically.
|| false)
//==============================
{
[AFTER END]
==================================================
[ControlXXX: allow omitting the 'Control' parameter][redundant if 'Control' is made omissible again for all ControlXXX functions in AHK v2]
Changed usage of Control parameter. · Lexikos/AutoHotkey_L@01f61cb · GitHub
https://github.com/Lexikos/AutoHotkey_L/commit/01f61cbd676026e29b15e8043cefc2260b288a9a
make Control parameter in ControlXXX functions optional
make this possible again:
hCtl := ControlGetHwnd("Edit1", "ahk_class Notepad")
vText := ControlGetText(, "ahk_id " hCtl)
MsgBox(vText)
[script.cpp]
[BEFORE]
BIFn(ControlAddItem, 2, 6, BIF_Control),
BIFn(ControlChoose, 2, 6, BIF_Control),
BIFn(ControlChooseString, 2, 6, BIF_Control),
BIF1(ControlClick, 0, 8),
BIFn(ControlDeleteItem, 2, 6, BIF_Control),
BIFn(ControlEditPaste, 2, 6, BIF_Control),
BIFn(ControlFindItem, 2, 6, BIF_ControlGet),
BIF1(ControlFocus, 1, 5),
BIFn(ControlGetChecked, 1, 5, BIF_ControlGet),
BIFn(ControlGetChoice, 1, 5, BIF_ControlGet),
BIF1(ControlGetClassNN, 1, 5),
BIFn(ControlGetCurrentCol, 1, 5, BIF_ControlGet),
BIFn(ControlGetCurrentLine, 1, 5, BIF_ControlGet),
BIFn(ControlGetEnabled, 1, 5, BIF_ControlGet),
BIFn(ControlGetExStyle, 1, 5, BIF_ControlGet),
BIF1(ControlGetFocus, 0, 4),
BIFn(ControlGetHwnd, 1, 5, BIF_ControlGet),
BIFn(ControlGetLine, 2, 6, BIF_ControlGet),
BIFn(ControlGetLineCount, 1, 5, BIF_ControlGet),
BIFn(ControlGetList, 0, 6, BIF_ControlGet),
BIF1(ControlGetPos, 0, 9, {1, 2, 3, 4}),
BIFn(ControlGetSelected, 1, 5, BIF_ControlGet),
BIFn(ControlGetStyle, 1, 5, BIF_ControlGet),
BIFn(ControlGetTab, 1, 5, BIF_ControlGet),
BIF1(ControlGetText, 1, 5),
BIFn(ControlGetVisible, 1, 5, BIF_ControlGet),
BIFn(ControlHide, 1, 5, BIF_Control),
BIFn(ControlHideDropDown, 1, 5, BIF_Control),
BIF1(ControlMove, 0, 9),
BIFn(ControlSend, 1, 6, BIF_ControlSend),
BIFn(ControlSendText, 1, 6, BIF_ControlSend),
BIFn(ControlSetChecked, 2, 6, BIF_Control),
BIFn(ControlSetEnabled, 2, 6, BIF_Control),
BIFn(ControlSetExStyle, 2, 6, BIF_Control),
BIFn(ControlSetStyle, 2, 6, BIF_Control),
BIFn(ControlSetTab, 2, 6, BIF_Control),
BIF1(ControlSetText, 2, 6),
BIFn(ControlShow, 1, 5, BIF_Control),
BIFn(ControlShowDropDown, 1, 5, BIF_Control),
[BEFORE END]
[AFTER]
//==============================
//jeeswg edit: code modified
BIFn(ControlAddItem, 1, 6, BIF_Control),
BIFn(ControlChoose, 1, 6, BIF_Control),
BIFn(ControlChooseString, 1, 6, BIF_Control),
BIF1(ControlClick, 0, 8),
BIFn(ControlDeleteItem, 1, 6, BIF_Control),
BIFn(ControlEditPaste, 1, 6, BIF_Control),
BIFn(ControlFindItem, 1, 6, BIF_ControlGet),
BIF1(ControlFocus, 0, 5),
BIFn(ControlGetChecked, 0, 5, BIF_ControlGet),
BIFn(ControlGetChoice, 0, 5, BIF_ControlGet),
BIF1(ControlGetClassNN, 0, 5),
BIFn(ControlGetCurrentCol, 0, 5, BIF_ControlGet),
BIFn(ControlGetCurrentLine, 0, 5, BIF_ControlGet),
BIFn(ControlGetEnabled, 0, 5, BIF_ControlGet),
BIFn(ControlGetExStyle, 0, 5, BIF_ControlGet),
BIF1(ControlGetFocus, 0, 4),
BIFn(ControlGetHwnd, 0, 5, BIF_ControlGet),
BIFn(ControlGetLine, 1, 6, BIF_ControlGet),
BIFn(ControlGetLineCount, 0, 5, BIF_ControlGet),
BIFn(ControlGetList, 0, 6, BIF_ControlGet),
BIF1(ControlGetPos, 0, 9, {1, 2, 3, 4}),
BIFn(ControlGetSelected, 0, 5, BIF_ControlGet),
BIFn(ControlGetStyle, 0, 5, BIF_ControlGet),
BIFn(ControlGetTab, 0, 5, BIF_ControlGet),
BIF1(ControlGetText, 0, 5),
BIFn(ControlGetVisible, 0, 5, BIF_ControlGet),
BIFn(ControlHide, 0, 5, BIF_Control),
BIFn(ControlHideDropDown, 0, 5, BIF_Control),
BIF1(ControlMove, 0, 9),
BIFn(ControlSend, 1, 6, BIF_ControlSend),
BIFn(ControlSendText, 1, 6, BIF_ControlSend),
BIFn(ControlSetChecked, 1, 6, BIF_Control),
BIFn(ControlSetEnabled, 1, 6, BIF_Control),
BIFn(ControlSetExStyle, 1, 6, BIF_Control),
BIFn(ControlSetStyle, 1, 6, BIF_Control),
BIFn(ControlSetTab, 1, 6, BIF_Control),
BIF1(ControlSetText, 1, 6),
BIFn(ControlShow, 0, 5, BIF_Control),
BIFn(ControlShowDropDown, 0, 5, BIF_Control),
//==============================
[AFTER END]
==================================================
[#IfWin: restore #IfWinActive][redundant if #If WinXXX() is optimised in AHK v1]
Removed #IfWin and optimized #If Win(). · Lexikos/AutoHotkey_L@e4a5493 · GitHub
https://github.com/Lexikos/AutoHotkey_L/commit/e4a5493749362b11c83ff71a9695782e72f82c65
allow this AHK v1 code in AHK v2:
#IfWinActive ahk_class Notepad
[ABOVE EDIT]
if (IS_DIRECTIVE_MATCH(_T("#IfTimeout")))
{
if (parameter)
g_HotExprTimeout = ATOU(parameter);
return CONDITION_TRUE;
}
[ABOVE EDIT END]
[EDIT]
//==============================
//jeeswg edit: code inserted
if (!_tcsnicmp(aBuf, _T("#IfWin"), 6))
{
HotCriterionType hot_criterion;
bool invert = !_tcsnicmp(aBuf + 6, _T("Not"), 3);
if (!_tcsnicmp(aBuf + (invert ? 9 : 6), _T("Active"), 6)) // It matches #IfWin[Not]Active.
hot_criterion = invert ? HOT_IF_NOT_ACTIVE : HOT_IF_ACTIVE;
else if (!_tcsnicmp(aBuf + (invert ? 9 : 6), _T("Exist"), 5))
hot_criterion = invert ? HOT_IF_NOT_EXIST : HOT_IF_EXIST;
else // It starts with #IfWin but isn't Active or Exist: Don't alter g->HotCriterion.
return CONDITION_FALSE; // Indicate unknown directive since there are currently no other possibilities.
if (!parameter) // The omission of the parameter indicates that any existing criteria should be turned off.
{
g->HotCriterion = NULL; // Indicate that no criteria are in effect for subsequent hotkeys.
return CONDITION_TRUE;
}
LPTSTR hot_win_title = parameter, hot_win_text; // Set default for title; text is determined later.
// Scan for the first non-escaped comma. If there is one, it marks the second parameter: WinText.
LPTSTR cp, first_non_escaped_comma;
for (first_non_escaped_comma = NULL, cp = hot_win_title; ; ++cp) // Increment to skip over the symbol just found by the inner for().
{
for (; *cp && !(*cp == g_EscapeChar || *cp == g_delimiter || *cp == g_DerefChar); ++cp); // Find the next escape char, comma, or %.
if (!*cp) // End of string was found.
break;
#define ERR_ESCAPED_COMMA_PERCENT _T("Literal commas and percent signs must be escaped (e.g. `%)")
if (*cp == g_DerefChar)
return ScriptError(ERR_ESCAPED_COMMA_PERCENT, aBuf);
if (*cp == g_delimiter) // non-escaped delimiter was found.
{
// Preserve the ability to add future-use parameters such as section of window
// over which the mouse is hovering, e.g. #IfWinActive, Untitled - Notepad,, TitleBar
if (first_non_escaped_comma) // A second non-escaped comma was found.
return ScriptError(ERR_ESCAPED_COMMA_PERCENT, aBuf);
// Otherwise:
first_non_escaped_comma = cp;
continue; // Check if there are any more non-escaped commas.
}
// Otherwise, an escape character was found, so skip over the next character (if any).
if (!*(++cp)) // The string unexpectedly ends in an escape character, so avoid out-of-bounds.
break;
// Otherwise, the ++cp above has skipped over the escape-char itself, and the loop's ++cp will now
// skip over the char-to-be-escaped, which is not the one we want (even if it is a comma).
}
if (first_non_escaped_comma) // Above found a non-escaped comma, so there is a second parameter (WinText).
{
// Omit whitespace to (seems best to conform to convention/expectations rather than give
// strange whitespace flexibility that would likely cause unwanted bugs due to inadvertently
// have two spaces instead of one). The user may use `s and `t to put literal leading/trailing
// spaces/tabs into these parameters.
hot_win_text = omit_leading_whitespace(first_non_escaped_comma + 1);
*first_non_escaped_comma = '\0'; // Terminate at the comma to split off hot_win_title on its own.
rtrim(hot_win_title, first_non_escaped_comma - hot_win_title); // Omit whitespace (see similar comment above).
// The following must be done only after trimming and omitting whitespace above, so that
// `s and `t can be used to insert leading/trailing spaces/tabs. ConvertEscapeSequences()
// also supports insertion of literal commas via escaped sequences.
ConvertEscapeSequences(hot_win_text, NULL);
}
else
hot_win_text = _T(""); // And leave hot_win_title set to the entire string because there's only one parameter.
// The following must be done only after trimming and omitting whitespace above (see similar comment above).
ConvertEscapeSequences(hot_win_title, NULL);
// The following also handles the case where both title and text are blank, which could happen
// due to something weird but legit like: #IfWinActive, ,
if (!SetHotkeyCriterion(hot_criterion, hot_win_title, hot_win_text))
return ScriptError(ERR_OUTOFMEM); // So rare that no second param is provided (since its contents may have been temp-terminated or altered above).
return CONDITION_TRUE;
} // Above completely handles all directives and non-directives that start with "#IfWin".
//==============================
[EDIT END]
[BELOW EDIT]
if (IS_DIRECTIVE_MATCH(_T("#Hotstring")))
[BELOW EDIT END]
==================================================
[Loop/Loop XXX: allow '% Expression'][redundant if Loop (expression)/LoopXXX are agreed for AHK v1/v2]
Changed commands to accept expressions by default. · Lexikos/AutoHotkey_L@4f8d55d · GitHub
https://github.com/Lexikos/AutoHotkey_L/commit/4f8d55dfddb6135c1d32584cb42ccefca0260e9d
allow this AHK v1 code in AHK v2:
Loop Parse, vText, % "`n", % "`r"
[ABOVE EDIT]
for (nArgs = mark = 0; action_args[mark] && nArgs < max_params; ++nArgs)
{
arg[nArgs] = action_args + mark;
arg_map[nArgs] = literal_map + mark;
is_expression = all_args_are_expressions; // This would be false for goto/gosub/break/continue.
[ABOVE EDIT END]
[EDIT]
//==============================
//jeeswg edit: code inserted
// The following implements the "% " prefix as a means of forcing an expression:
if (is_expression
//&& aActionType == ACT_LOOP
&& (aActionType == ACT_LOOP || aActionType == ACT_LOOP_FILE || aActionType == ACT_LOOP_PARSE || aActionType == ACT_LOOP_READ || aActionType == ACT_LOOP_REG)
&& *arg[nArgs] == g_DerefChar && !*arg_map[nArgs] // It's a non-literal deref character.
//&& *arg[nArgs] == 'x' && !*arg_map[nArgs] // It's a non-literal deref character.
&& IS_SPACE_OR_TAB(arg[nArgs][1])) // Followed by a space or tab.
{
//Beep(3000, 300);
// Skip the "% " prefix.
arg[nArgs] = action_args + mark + 2;
arg_map[nArgs] = literal_map + mark + 2;
mark += 2;
//nArgs += 2;
//continue;
}
//==============================
[EDIT END]
[BELOW EDIT]
// Find the end of the above arg:
if (is_expression)
// Find the next delimiter, taking into account quote marks, parentheses, etc.
mark = FindExprDelim(action_args, g_delimiter, mark, literal_map);
else
// Find the next non-literal delimiter.
mark = FindTextDelim(action_args, g_delimiter, mark, literal_map);
[BELOW EDIT END]
==================================================