Let players remap hotkeys in-game, fix default hotkeys being qwerty-specific.

- Provide a "Hotkey" screen to let players remap hotkeys in-game using a
convenient setup.
- Make all .cfg hotkeys refer to scancodes (i.e. position on the
keyboard), so that default hotkeys now translate correctly for AZERTY,
QWERTZ and other layouts.
- 'BackSpace' is now an alias for 'Delete', and works for killing units.
This fixes #1917, as macs don't have a proper delete key and would need
to use Fn+Del otherwise. This shifts "timewarp" to Shift+BackSpace.

Functionally, this switches hotkeys to scancodes, as that makes more
sense (they are combinations of key positions, not actual text output).
SDL includes are cleaned and key names are reused.

Fixes #2850, Fixes #2604, Refs #1810, Fixes #1917.

Follows work in 3d7784d2af.

Various diffs tested by: Angen, Stan, Freagarach
Comments by: Nescio
Differential Revision: https://code.wildfiregames.com/D2814
This was SVN commit r24215.
This commit is contained in:
wraitii 2020-11-19 09:27:26 +00:00
parent e6adfd01bf
commit a4852c4c01
38 changed files with 1585 additions and 534 deletions

View file

@ -141,7 +141,7 @@ menu = 60 ; Throttle FPS in menus only.
; See keys.txt for the list of key names.
; > SYSTEM SETTINGS
exit = "Ctrl+Break", "Super+Q" ; Exit to desktop
exit = "Ctrl+Break", "Super+Q", "Alt+F4" ; Exit to desktop
cancel = Escape ; Close or cancel the current dialog box/popup
confirm = Return ; Confirm the current command
pause = Pause ; Pause/unpause game
@ -186,8 +186,8 @@ quickload = "Shift+F8"
reset = "R" ; Reset camera rotation to default.
follow = "F" ; Follow the first unit in the selection
rallypointfocus = unused ; Focus the camera on the rally point of the selected building
zoom.in = Plus, Equals, NumPlus ; Zoom camera in (continuous control)
zoom.out = Minus, Underscore, NumMinus ; Zoom camera out (continuous control)
zoom.in = Plus, NumPlus ; Zoom camera in (continuous control)
zoom.out = Minus, NumMinus ; Zoom camera out (continuous control)
zoom.wheel.in = WheelUp ; Zoom camera in (stepped control)
zoom.wheel.out = WheelDown ; Zoom camera out (stepped control)
rotate.up = "Ctrl+UpArrow", "Ctrl+W" ; Rotate camera to look upwards
@ -238,15 +238,15 @@ save = "Shift+F11" ; Save current profiler data to logs/profile.txt
toggle = "Ctrl+F11" ; Enable/disable HTTP/GPU modes for new profiler
[hotkey.selection]
cancel = Esc ; Un-select all units and cancel building placement
add = Shift ; Add units to selection
militaryonly = Alt ; Add only military units to the selection
nonmilitaryonly = "Alt+Y" ; Add only non-military units to the selection
idleonly = "I" ; Select only idle units
woundedonly = "O" ; Select only wounded units
remove = Ctrl ; Remove units from selection
cancel = Esc ; Un-select all units and cancel building placement
idleworker = Period, NumPoint ; Select next idle worker
idlewarrior = ForwardSlash, NumDivide ; Select next idle warrior
idleworker = Period, NumDecimal ; Select next idle worker
idlewarrior = Slash, NumDivide ; Select next idle warrior
idleunit = BackSlash ; Select next idle unit
offscreen = Alt ; Include offscreen units in selection
[hotkey.selection.group.add]
@ -284,7 +284,7 @@ offscreen = Alt ; Include offscreen units in selection
9 = 9, Num9
[hotkey.session]
kill = Delete ; Destroy selected units
kill = Delete, Backspace ; Destroy selected units
stop = "H" ; Stop the current action
backtowork = "Y" ; The unit will go back to work
unload = "U" ; Unload garrisoned units when a building/mechanical unit is selected
@ -312,8 +312,8 @@ snaptoedges = Ctrl ; Modifier to align new structures with nearby exis
; Overlays
showstatusbars = Tab ; Toggle display of status bars
devcommands.toggle = "Alt+D" ; Toggle developer commands panel
highlightguarding = PgDn ; Toggle highlight of guarding units
highlightguarded = PgUp ; Toggle highlight of guarded units
highlightguarding = PageDown ; Toggle highlight of guarding units
highlightguarded = PageUp ; Toggle highlight of guarded units
diplomacycolors = "Alt+X" ; Toggle diplomacy colors
toggleattackrange = "Alt+C" ; Toggle display of attack range overlays of selected defensive structures
toggleaurasrange = "Alt+V" ; Toggle display of aura range overlays of selected units and structures
@ -328,7 +328,7 @@ objectives.toggle = "Ctrl+O" ; Toggle in-game objectives page
tutorial.toggle = "Ctrl+P" ; Toggle in-game tutorial panel
[hotkey.session.savedgames]
delete = Delete ; Delete the selected saved game asking confirmation
delete = Delete, Backspace ; Delete the selected saved game asking confirmation
noconfirmation = Shift ; Do not ask confirmation when deleting a game
[hotkey.session.queueunit] ; > UNIT TRAINING
@ -343,7 +343,7 @@ noconfirmation = Shift ; Do not ask confirmation when deleting a game
[hotkey.session.timewarp]
fastforward = Space ; If timewarp mode enabled, speed up the game
rewind = Backspace ; If timewarp mode enabled, go back to earlier point in the game
rewind = "Shift+Backspace" ; If timewarp mode enabled, go back to earlier point in the game
[hotkey.tab]
next = "Tab", "Alt+S" ; Show the next tab

View file

@ -1,107 +1,39 @@
## This file documents keynames that can be used in .cfg files for specifying hotkeys
## Note: all are case-insensitive.
## Note: the keynames are not actually configured or implemented here
## Also note: if your keyboard supports keys that do not appear here (such as the French é letter),
## you should be able to use it directly.
## Note: those names map to 'scancodes', i.e. positions on a classic QWERTY keyboard.
## The in-game hotkey editor will show you what that corresponds to.
Backspace, BkSp
Tab
Clear
Return, Ret
Pause
MouseLeft
MouseRight
MouseMiddle
MouseX1
MouseX2
WheelUp
WheelDown
WheelLeft
WheelRight
A-Z
0-9
Return, Enter
Break
Escape, Esc
Space, Spc
!, Exclaim
", DoubleQuote
#, Hash
$, Dollar
&, Ampersand
', SingleQuote
(, LeftParen
), RightParen
*, Asterisk
+, Plus
,, Comma
Backspace
Tab
Space
-, Minus
., Period
/, ForwardSlash
0
1
2
3
4
5
6
7
8
9
:, Colon
;, Semicolon
<, LessThan
=, Equals
>, GreaterThan
?, Question
@, At
=, Plus, Equals
[, LeftBracket
\, BackSlash
], RightBracket
^, Caret
_, Underscore
\\, Backslash ## You do need the espacing in the first case.
;, Semicolon
', Quote ## Maps to the quote/doubleQuote key on QWERTY.
`, BackQuote
A
B
C
D
E
F
G
H
I
J
K
L
M
N
O
P
Q
R
S
T
U
V
W
X
Y
Z
Delete, Del
Numpad 0, Num0
Numpad 1, Num1
Numpad 2, Num2
Numpad 3, Num3
Numpad 4, Num4
Numpad 5, Num5
Numpad 6, Num6
Numpad 7, Num7
Numpad 8, Num8
Numpad 9, Num9
Numpad ., NumPoint
Numpad /, NumDivide
Numpad *, NumMultiply
Numpad -, NumMinus
Numpad +, NumPlus
Numpad Enter, NumEnter
Numpad =, NumEquals
Arrow Up, UpArrow
Arrow Down, DownArrow
Arrow Right, RightArrow
Arrow Left, LeftArrow
Insert, Ins
Home
End
Page Up, PgUp
Page Down, PgDn
,, Comma
., Period
/, Slash
F1
F2
F3
@ -114,41 +46,155 @@ F9
F10
F11
F12
PrintScreen
ScrollLock
Pause
Insert
Home
PageUp
Delete, Del
End
PageDown
Right, RightArrow
Left, LeftArrow
Down, DownArrow
Up, UpArrow
Numlock
Keypad /, NumDivide
Keypad *, NumMultiply
Keypad -, NumMinus
Keypad +, NumPlus
Keypad Enter, NumEnter
Keypad 1, Num1
Keypad 2, Num2
Keypad 3, Num3
Keypad 4, Num4
Keypad 5, Num5
Keypad 6, Num6
Keypad 7, Num7
Keypad 8, Num8
Keypad 9, Num9
Keypad 0, Num0
Keypad ., NumDecimal
Application
Power
Keypad =
F13
F14
F15
Num Lock, NumLock
Caps Lock, CapsLock
Scroll Lock, ScrlLock
Right Shift, RightShift
Left Shift, LeftShift
Shift, AnyShift
Right Ctrl, RightCtrl
Left Ctrl, LeftCtrl
Ctrl, AnyCtrl
Right Alt, RightAlt
Left Alt, LeftAlt
Alt, AnyAlt
Left Super, LeftWin
Right Super, RightWin
Super, AnyWindows ## Windows key, also Command/meta key on Macs
Alt Gr, AltGr
Compose
F16
F17
F18
F19
F20
F21
F22
F23
F24
Execute
Help
Print Screen, PrtSc
SysRq
Break
Menu
Power
Euro
Select
Stop
Again
Undo
Left Mouse Button, MouseLeft
Right Mouse Button, MouseRight
Middle Mouse Button, MouseMiddle
Mouse Wheel Up, WheelUp
Mouse Wheel Down, WheelDown
MouseButtonX, MouseNX # where X is a number 1-255, for extra mouse buttons
## (note that some mice start numbering their buttons at higher numbers
## so the first extra button on your mouse might be #8 here)
Cut
Copy
Paste
Find
Mute
VolumeUp
VolumeDown
Keypad ,
AltErase
SysReq
Cancel
Clear
Prior
Return
Separator
Out
Oper
Clear / Again
CrSel
ExSel
Keypad 00
Keypad 000
ThousandsSeparator
DecimalSeparator
CurrencyUnit
CurrencySubUnit
Keypad (
Keypad )
Keypad {
Keypad }
Keypad Tab
Keypad Backspace
Keypad A
Keypad B
Keypad C
Keypad D
Keypad E
Keypad F
Keypad XOR
Keypad ^
Keypad %
Keypad <
Keypad >
Keypad &
Keypad &&
Keypad |
Keypad ||
Keypad :
Keypad #
Keypad Space
Keypad @
Keypad !
Keypad MemStore
Keypad MemRecall
Keypad MemClear
Keypad MemAdd
Keypad MemSubtract
Keypad MemMultiply
Keypad MemDivide
Keypad +/-
Keypad Clear
Keypad ClearEntry
Keypad Binary
Keypad Octal
Keypad Decimal
Keypad Hexadecimal
Left Ctrl, Ctrl
Left Shift, Shift
Left Alt, Alt
Left GUI, Super
Right Ctrl, Ctrl
Right Shift, Shift
Right Alt, Alt
Right GUI, Super
ModeSwitch
AudioNext
AudioPrev
AudioStop
AudioPlay
AudioMute
MediaSelect
WWW
Mail
Calculator
Computer
AC Search
AC Home
AC Back
AC Forward
AC Stop
AC Refresh
AC Bookmarks
BrightnessDown
BrightnessUp
DisplaySwitch
KBDIllumToggle
KBDIllumDown
KBDIllumUp
Eject
Sleep

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ece26cb75be5a95695c229025e8234b80422c9652c0c3b9087bd0667d1b99ab1
size 436

View file

@ -0,0 +1,33 @@
/**
* Holds a map of scancode name -> user keyboard name
*/
var g_ScancodesMap;
function hotkeySort(a, b)
{
const specialKeys = ["Shift", "Alt", "Ctrl", "Super"];
// Quick hack to put those first.
if (specialKeys.indexOf(a) !== -1)
a = ' ' + a;
if (specialKeys.indexOf(b) !== -1)
b = ' ' + b;
return a.localeCompare(b, Engine.GetCurrentLocale().substr(0, 2), { "numeric": true });
}
function formatHotkeyCombination(comb, translateScancodes = true)
{
if (!translateScancodes)
return comb.sort(hotkeySort).join("+");
if (!g_ScancodesMap)
g_ScancodesMap = Engine.GetScancodeKeyNames();
return comb.sort(hotkeySort).map(hk => g_ScancodesMap[hk]).join("+");
}
function formatHotkeyCombinations(combinations, translateScancodes = true)
{
let combs = combinations.map(x => formatHotkeyCombination(x, translateScancodes));
combs.sort((a, b) => a.length - b.length || a - b);
return translateScancodes ? combs.join(", ") : combs;
}

View file

@ -0,0 +1,113 @@
/**
* Handle the interface to pick a hotkey combination.
* The player must keep a key combination for 2s in the input field for it to be registered.
*/
class HotkeyPicker
{
constructor(onClose, name, combinations)
{
this.name = name;
this.combinations = combinations;
this.window = Engine.GetGUIObjectByName("hotkeyPicker");
this.window.hidden = false;
this.enteringInput = -1;
Engine.GetGUIObjectByName("hotkeyPickerTitle").caption = translate(this.name);
this.setupCombinations();
this.render();
Engine.GetGUIObjectByName("hotkeyPickerReset").onPress = () => {
// This is a bit "using a bazooka to kill a fly"
Engine.ConfigDB_RemoveValue("user", "hotkey." + this.name);
Engine.ConfigDB_WriteFile("user", "config/user.cfg");
Engine.ReloadHotkeys();
let data = Engine.GetHotkeyMap();
this.combinations = data[this.name];
this.setupCombinations();
this.render();
};
Engine.GetGUIObjectByName("hotkeyPickerCancel").onPress = () => {
onClose(this, false);
};
Engine.GetGUIObjectByName("hotkeyPickerSave").onPress = () => {
onClose(this, true);
};
}
setupCombinations()
{
for (let i = 0; i < 4; ++i)
{
let s = Engine.GetGUIObjectByName("combination[" + i + "]").size;
s.top = +i * 60 + 90;
s.bottom = +i * 60 + 120;
Engine.GetGUIObjectByName("combination[" + i + "]").size = s;
Engine.GetGUIObjectByName("combNb[" + i + "]").caption = sprintf(translate("#%i"), i);
if (i == this.combinations.length)
this.combinations.push([]);
let input = Engine.GetGUIObjectByName("combMapping[" + i + "]");
let picker = Engine.GetGUIObjectByName("picker[" + i + "]");
Engine.GetGUIObjectByName("combMappingBtn[" + i + "]").onPress = () => {
this.enteringInput = i;
picker.focus();
this.render();
};
picker.onKeyChange = keys => {
input.caption = (keys.length ?
formatHotkeyCombination(keys) + translate(" (hold to register)") :
translate("Enter new Hotkey, hold to register."));
};
Engine.GetGUIObjectByName("deleteComb[" + i + "]").onPress = (j => () => {
this.combinations[j] = [];
this.render();
})(i);
picker.onCombination = (j => keys => {
this.combinations[j] = keys;
this.enteringInput = -1;
picker.blur();
this.render();
})(i);
}
}
close()
{
this.window.hidden = true;
for (let i = 0; i < 4; ++i)
Engine.GetGUIObjectByName("picker[" + i + "]").blur();
}
render()
{
for (let i = 0; i < 4; ++i)
{
let input = Engine.GetGUIObjectByName("combMapping[" + i + "]");
if (i == this.enteringInput)
input.caption = translate("Enter new Hotkey, hold to register.");
else
input.caption = formatHotkeyCombination(this.combinations[i]) || "(unused)";
Engine.GetGUIObjectByName("conflicts[" + i + "]").caption = "";
Engine.GetGUIObjectByName("deleteComb[" + i + "]").hidden = !this.combinations[i].length;
let conflicts = (Engine.GetConflicts(this.combinations[i]) || [])
.filter(name => name != this.name).map(translate);
if (conflicts.length)
Engine.GetGUIObjectByName("conflicts[" + i + "]").caption =
coloredText(translate("May conflict with: "), "255 153 0") + conflicts.join(", ");
}
// Gray out buttons when entering an input so it's obvious clicking on them will do nothing.
Engine.GetGUIObjectByName("hotkeyPickerReset").enabled = this.enteringInput == -1;
Engine.GetGUIObjectByName("hotkeyPickerCancel").enabled = this.enteringInput == -1;
Engine.GetGUIObjectByName("hotkeyPickerSave").enabled = this.enteringInput == -1;
}
}

View file

@ -0,0 +1,177 @@
class HotkeysPage
{
constructor()
{
Engine.GetGUIObjectByName("hotkeyList").onMouseLeftDoubleClickItem = () => {
let idx = Engine.GetGUIObjectByName("hotkeyList").selected;
let picker = new HotkeyPicker(
this.onHotkeyPicked.bind(this),
Engine.GetGUIObjectByName("hotkeyList").list_data[idx],
this.hotkeys[Engine.GetGUIObjectByName("hotkeyList").list_data[idx]]
);
};
Engine.GetGUIObjectByName("hotkeyFilter").onSelectionChange = () => this.setupHotkeyList();
Engine.GetGUIObjectByName("hotkeyTextFilter").onTextEdit = () => this.setupHotkeyList();
Engine.GetGUIObjectByName("hotkeyClose").onPress = () => Engine.PopGuiPage();
Engine.GetGUIObjectByName("hotkeyReset").onPress = () => this.resetUserHotkeys();
Engine.GetGUIObjectByName("hotkeySave").onPress = () => {
this.saveUserHotkeys();
};
this.setupHotkeyData();
this.setupFilters();
this.setupHotkeyList();
}
setupFilters()
{
let dropdown = Engine.GetGUIObjectByName("hotkeyFilter");
let names = [];
for (let cat in this.categories)
names.push(this.categories[cat].label);
dropdown.list = [translate("All Hotkeys")].concat(names);
dropdown.list_data = [-1].concat(Object.keys(this.categories));
dropdown.selected = 0;
}
setupHotkeyList()
{
let hotkeyList = Engine.GetGUIObjectByName("hotkeyList");
hotkeyList.selected = -1;
let textFilter = Engine.GetGUIObjectByName("hotkeyTextFilter").caption;
let dropdown = Engine.GetGUIObjectByName("hotkeyFilter");
if (dropdown.selected && dropdown.selected !== 0)
{
let category = this.categories[dropdown.list_data[dropdown.selected]];
// This is inefficient but it seems fast enough.
let hotkeys = category.hotkeys.filter(x => translate(x[0]).indexOf(textFilter) !== -1);
hotkeyList.list_name = hotkeys.map(x => translate(x[0]));
hotkeyList.list_mapping = hotkeys.map(x => formatHotkeyCombinations(x[1]));
hotkeyList.list = hotkeys.map(() => 0);
hotkeyList.list_data = hotkeys.map(x => x[0]);
}
else
{
// TODO SM62+ : refactor using flat()
let flattened = [];
for (let cat in this.categories)
flattened = flattened.concat(this.categories[cat].hotkeys);
flattened = flattened.filter(x => translate(x[0]).indexOf(textFilter) !== -1);
hotkeyList.list_name = flattened.map(x => translate(x[0]));
hotkeyList.list_mapping = flattened.map(x => formatHotkeyCombinations(x[1]));
hotkeyList.list = flattened.map(() => 0);
hotkeyList.list_data = flattened.map(x => x[0]);
}
}
onHotkeyPicked(picker, success)
{
picker.close();
if (!success)
return;
// Remove empty combinations which the picker added.
picker.combinations = picker.combinations.filter(x => x.length);
this.hotkeys[picker.name] = picker.combinations;
// Have to find the correct line.
let panel = Engine.GetGUIObjectByName("hotkeyList");
for (let cat in this.categories)
{
let idx = this.categories[cat].hotkeys.findIndex(([name, _]) => name == picker.name);
if (idx === -1)
continue;
this.categories[cat].hotkeys[idx][1] = picker.combinations;
}
this.setupHotkeyList();
}
setupHotkeyData()
{
let hotkeydata = Engine.GetHotkeyMap();
this.hotkeys = hotkeydata;
let categories = {
"other": {
"label": translate("Other hotkeys"),
"hotkeys": []
}
};
let n_categories = 1;
for (let hotkeyName in this.hotkeys)
{
let category = "other";
let firstdot = hotkeyName.indexOf('.');
if (firstdot !== -1)
category = hotkeyName.substr(0, firstdot);
if (!(category in categories))
{
if (n_categories > 18)
category = "other";
categories[category] = {
"label": category,
"hotkeys": []
};
}
categories[category].hotkeys.push([hotkeyName, this.hotkeys[hotkeyName]]);
}
// Remove categories that are too small to deserve a tab.
for (let cat of Object.keys(categories))
if (categories[cat].hotkeys.length < 3)
{
categories.other.hotkeys = categories.other.hotkeys.concat(categories[cat].hotkeys);
delete categories[cat];
}
for (let cat in categories)
categories[cat].hotkeys = categories[cat].hotkeys.sort();
this.categories = categories;
}
resetUserHotkeys()
{
messageBox(
400, 200,
translate("Reset all hotkeys to default values?"),
translate("Confirmation"),
[translate("No"), translate("Yes")],
[
() => {},
() => {
for (let cat in this.categories)
this.categories[cat].hotkeys.forEach(([name, _]) => {
Engine.ConfigDB_RemoveValue("user", "hotkey." + name);
});
Engine.ConfigDB_WriteFile("user", "config/user.cfg");
Engine.ReloadHotkeys();
this.setupHotkeyData();
this.setupHotkeyList();
}
]);
}
saveUserHotkeys()
{
for (let hotkey in this.hotkeys)
Engine.ConfigDB_RemoveValue("user", "hotkey." + hotkey);
Engine.ReloadHotkeys();
let defaultData = Engine.GetHotkeyMap();
for (let hotkey in this.hotkeys)
{
let keymap = formatHotkeyCombinations(this.hotkeys[hotkey], false);
if (keymap.join("") !== formatHotkeyCombinations(defaultData[hotkey], false).join(""))
Engine.ConfigDB_CreateValues("user", "hotkey." + hotkey, keymap);
}
Engine.ConfigDB_WriteFile("user", "config/user.cfg");
Engine.ReloadHotkeys();
}
}
function init(data)
{
let hotkeyPage = new HotkeysPage(data);
}

View file

@ -0,0 +1,112 @@
<?xml version="1.0" encoding="utf-8"?>
<objects>
<script directory="gui/common/"/>
<script directory="gui/hotkeys/"/>
<object type="image" sprite="BackgroundTranslucent"/>
<!-- Hotkey List Window -->
<object name="hotkeys" type="image" style="ModernDialog" size="50%-350 50%-344 50%+350 50%+344">
<object style="ModernLabelText" type="text" size="50%-128 -16 50%+128 16">
<translatableAttribute id="caption">Hotkeys</translatableAttribute>
</object>
<object style="ModernLabelText" type="text" size="32 32 132 58" text_align="left">
<translatableAttribute id="caption">Category:</translatableAttribute>
</object>
<object name="hotkeyFilter" type="dropdown" style="ModernDropDown" size="132 32 300 58"/>
<object style="ModernLabelText" type="text" size="100%-300 32 100%-200 58" text_align="left">
<translatableAttribute id="caption">Filter:</translatableAttribute>
</object>
<object name="hotkeyTextFilter" type="input" style="ModernInput" size="100%-200 32 100%-32 58"/>
<object name="hotkeyList"
style="ModernSortedList"
selected_column="name"
selected_column_order="-1"
sortable="false"
size="32 70 100%-32 100%-70"
type="olist"
auto_scroll="true"
>
<column id="name" color="255 255 255" width="60%">
<translatableAttribute id="heading" context="hotkey list">Name</translatableAttribute>
</column>
<column id="mapping" color="255 255 255" width="40%">
<translatableAttribute id="heading" context="hotkey list">Mapping</translatableAttribute>
</column>
</object>
<object name="hotkeyReset" type="button" size="32 100%-52 188 100%-24" style="ModernButtonRed">
<translatableAttribute id="caption">Reset</translatableAttribute>
<translatableAttribute id="tooltip">Resets user settings to their game default</translatableAttribute>
</object>
<object name="hotkeySave" type="button" size="100%-352 100%-52 100%-196 100%-24" style="ModernButtonRed">
<translatableAttribute id="caption">Save</translatableAttribute>
<translatableAttribute id="tooltip">Saves changes</translatableAttribute>
</object>
<object name="hotkeyClose" type="button" size="100%-188 100%-52 100%-32 100%-24" style="ModernButtonRed" hotkey="cancel">
<translatableAttribute id="caption">Close</translatableAttribute>
<translatableAttribute id="tooltip">Unsaved changes will be lost</translatableAttribute>
</object>
</object>
<!-- Hotkey Picker popup -->
<object name="hotkeyPicker" type="image" sprite="BackgroundTranslucent" hidden="true" z="100">
<object type="image"
style="ModernDialog"
size="50%-300 50%-190 50%+300 50%+190"
>
<object name="hotkeyPickerTitle" style="ModernLabelText" type="text" size="50%-128 -16 50%+128 16">
<translatableAttribute id="caption">Hotkey</translatableAttribute>
</object>
<object name="hotkeyPickerDesc" style="ModernLabelText" type="text" size="8 20 100%-8 66">
<translatableAttribute id="caption">Click on any mapping to modify it.\n You may have up to 4 different hotkeys.</translatableAttribute>
</object>
<repeat count="4" var="n">
<object name="combination[n]" size="8 40 100%-8 70" hidden="false">
<object name="combNb[n]" style="ModernLabelText" type="text" size="0 0 20 100%" text_align="left"/>
<object name="picker[n]" type="hotkeypicker"/>
<object name="combMapping[n]" style="ModernInput" type="input" readonly="true" size="30 2 60% 100%-2"/>
<!-- Used to detect clicks on the input. -->
<object name="combMappingBtn[n]" type="button" size="30 2 60% 100%-2">
<translatableAttribute id="tooltip">Click to set the hotkey</translatableAttribute>
</object>
<object name="deleteComb[n]" type="button" size="60%+10 5 60%+30 100%-5"
sprite="crossRed">
<translatableAttribute id="tooltip">Click to delete the hotkey</translatableAttribute>
</object>
<object name="conflicts[n]" style="ModernLabelText" type="text" size="60%+40 2 100%-8 100%-2" clip="false" text_valign="center"/>
</object>
</repeat>
<object name="hotkeyPickerReset" type="button" size="16 100%-52 172 100%-24" style="ModernButtonRed">
<translatableAttribute id="caption">Reset</translatableAttribute>
<translatableAttribute id="tooltip">Resets user settings to their game default</translatableAttribute>
</object>
<object name="hotkeyPickerSave" type="button" size="100%-336 100%-52 100%-180 100%-24" style="ModernButtonRed">
<translatableAttribute id="caption">Accept</translatableAttribute>
<translatableAttribute id="tooltip">Change the hotkeys and close</translatableAttribute>
</object>
<object name="hotkeyPickerCancel" type="button" size="100%-172 100%-52 100%-16 100%-24" style="ModernButtonRed" hotkey="cancel">
<translatableAttribute id="caption">Cancel</translatableAttribute>
<translatableAttribute id="tooltip">The hotkeys will not be modified</translatableAttribute>
</object>
</object>
</object>
</objects>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<page>
<include>common/modern/setup.xml</include>
<include>common/modern/styles.xml</include>
<include>common/modern/sprites.xml</include>
<include>common/setup.xml</include>
<include>common/sprites.xml</include>
<include>common/styles.xml</include>
<include>common/global.xml</include>
<include>hotkeys/sprites.xml</include>
<include>hotkeys/hotkeys.xml</include>
</page>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<sprites>
<sprite name="crossRed">
<effect add_color="180 0 0"/>
<image
texture="global/icon/cross.png"
size="0 0 100% 100%"
/>
</sprite>
</sprites>

View file

@ -7,13 +7,13 @@ You can switch between fullscreen and windowed mode by pressing Alt + Enter. In
[font="sans-bold-16"]Playing the game[font="sans-14"]
The controls and gameplay should be familiar to players of traditional real-time strategy (RTS) games. There are currently a lot of missing features and poorly-balanced stats you will probably have to wait until a beta release for it to work well.
Basic controls:
Basic controls (by default):
• Left-click to select units
• Left-click-and-drag to select groups of units
• Right-click to order units to the target
• Arrow keys or WASD keys to move the camera
• Ctrl + arrow keys, or shift + mouse wheel, to rotate the camera
• Mouse wheel, or “=”/“+” and “-”/“−” keys, to zoom
• Mouse wheel, or “hotkey.camera.zoom.in” and ““hotkey.camera.zoom.out” keys, to zoom
[font="sans-bold-16"]Modes[font="sans-14"]
The main menu gives access to two game modes:
@ -37,122 +37,118 @@ Finally change the settings. For random maps this includes the number of players
When you are ready to start, click the “Start game” button.
[font="sans-bold-16"]Hotkeys[font="sans-14"]
You may change hotkeys in [font="sans-bold-14"]Options > Hotkeys[font="sans-14"] to suit your liking.
[font="sans-bold-14"]Program-wide[font="sans-14"]
Alt + F4 Immediately close the game, without asking for confirmation
Alt + Enter (Return) Toggle between fullscreen and windowed
~ or F9 Toggle console
Alt + F Toggle frame counter (FPS)
F11 Toggle real-time profiler (cycles through the displays of information)
Shift + F11 Save current profiler data to “logs/profile.txt”
F2 Take screenshot (in .png format, location is displayed in the top left of the GUI after the file has been saved, and can also be seen in the console/logs if you miss it there)
Shift + F2 Take huge screenshot (6400×4800 pixels, in .bmp format, location is displayed in the top left of the GUI after the file has been saved, and can also be seen in the console/logs if you miss it there)
Tab, Alt + S Switch to the next tab
Shift + Tab, Alt + W Switch to the previous tab
hotkey.exit Immediately close the game, without asking for confirmation
hotkey.togglefullscreen Toggle between fullscreen and windowed
hotkey.console.toggle Toggle console
hotkey.fps.toggle Toggle frame counter (FPS)
hotkey.profile.toggle Toggle real-time profiler (cycles through the displays of information)
hotkey.profile.save Save current profiler data to “logs/profile.txt”
hotkey.screenshot Take screenshot (in .png format, location is displayed in the top left of the GUI after the file has been saved, and can also be seen in the console/logs if you miss it there)
hotkey.bigscreenshot Take huge screenshot (6400×4800 pixels, in .bmp format, location is displayed in the top left of the GUI after the file has been saved, and can also be seen in the console/logs if you miss it there)
hotkey.tab.next Switch to the next tab
hotkey.tab.prev Switch to the previous tab
[font="sans-bold-14"]In Game[font="sans-14"]
Double Left Click \[on unit] Select all of your units of the same kind on the screen (even if they're different ranks)
Triple Left Click \[on unit] Select all of your units of the same kind and the same rank on the screen
Alt + Double Left Click \[on unit] Select all your units of the same kind on the entire map (even if they have different ranks)
Alt + Triple Left Click \[on unit] Select all your units of the same kind and rank on the entire map
Shift + F5 Quicksave
Shift + F8 Quickload
F10 Toggle menu
F12 Toggle time elapsed counter
Esc Close all dialogs (chat, menu) or clear selected units
Enter (Return) Open chat or send message
T Send team chat
L Chat with the previously selected private chat partner
Pause Pause or resume the game
Delete Delete currently selected unit(s)/structure(s), ask for confirmation
Shift + Delete Immediately delete currently selected unit(s)/structure(s), without asking for confirmation
/ (Slash) Select idle fighter
Shift + / (Slash) Add idle fighter to selection
Alt + / (Slash) Select all idle fighters
. (Period) Select idle worker (including citizen-soldiers)
Shift + . (Period) Add idle worker to selection (including citizen-soldiers)
Alt + . (Period) Select all idle workers (including citizen-soldiers)
\\ (Backslash) Select idle unit
Shift + \\ (Backslash) Add idle unit to selection
Alt + \\ (Backslash) Select all idle units
H Stop (halt) the currently selected units
Y The unit will go back to work
U Unload the garrisoned units of the selected structure(s)
Ctrl + 1 (and so on up to Ctrl + 0) Create control group 1 (to 0) from the selected unit(s)/structure(s)
1 (and so on up to 0) Select the unit(s)/structure(s) in control group 1 (to 0)
Shift + 1 (to 0) Add control group 1 (to 0) to the selected unit(s)/structure(s)
Ctrl + F5 (and so on up to F8) Mark the current camera position, for jumping back to later
F5 (and so on up to F8) Move the camera to a marked position. Jump back to the last location if the camera is already over the marked position
Z, X, C, V, B, N, M With training structure selected. Add the 1st, 2nd, … unit shown to the training queue for all the selected structures
PageUp with unit(s) selected Highlight the unit(s)/structure(s) guarded by the selection
PageDown with unit(s)/structure(s) selected Highlight the unit(s) guarding the selection
Tab See all status bars (which would also show the building progress)
Ctrl + Tab Toggle summary window
Alt + L Show the multiplayer lobby in a dialog window
Ctrl + H Toggle in-game diplomacy panel
Ctrl + B Toggle in-game barter and trade panel
Ctrl + O Toggle in-game objectives panel
Ctrl + P Toggle in-game tutorial panel
Alt + Shift + T Toggle structure tree panel
Alt + Shift + H Toggle civilization info panel
hotkey.quicksave Quicksave
hotkey.quickload Quickload
hotkey.session.gui.menu.toggle Toggle menu
hotkey.timeelapsedcounter.toggle Toggle time elapsed counter
hotkey.cancel Close all dialogs (chat, menu)
hotkey.confirm Open chat or send message
hotkey.teamchat Send team chat
hotkey.privatechat Chat with the previously selected private chat partner
hotkey.pause Pause or resume the game
hotkey.session.kill Delete currently selected unit(s)/structure(s), ask for confirmation
• With hotkey.session.noconfirmation Immediately delete currently selected unit(s)/structure(s), without asking for confirmation
hotkey.selection.add Modifier - add to selection (works with clicking and hotkeys, e.g. the idle hotkeys)
hotkey.selection.remove Modifier - remove from selection (works with clicking and hotkeys, e.g. the idle hotkeys)
hotkey.selection.offscreen - Modifier - add all units, including offscreen units, to selection.
hotkey.selection.cancel Unselect all units, cancel building placement.
hotkey.selection.idlewarrior Select idle fighter
hotkey.selection.idleworker Select idle worker (including citizen-soldiers)
hotkey.selection.idleunit Select idle unit
hotkey.session.stop Stop (halt) the currently selected units
hotkey.session.backtowork The unit will go back to work
hotkey.session.unload Unload the garrisoned units of the selected structure(s)
hotkey.selection.group.save.1 Create control group 1 (by default, respectively 2,3, ...) from the selected unit(s)/structure(s)
hotkey.selection.group.select.1 - Select the unit(s)/structure(s) in control group 1
hotkey.selection.group.add.1 Add control group 1 (to 0) to the selected unit(s)/structure(s)
hotkey.camera.jump.set.1 Mark the current camera position, for jumping back to later (by default, there are 4 possible positions)
hotkey.camera.jump.1 Move the camera to a marked position. Jump back to the last location if the camera is already over the marked position
hotkey.session.queueunit.1, hotkey.session.queueunit.2, hotkey.session.queueunit.3, hotkey.session.queueunit.4, hotkey.session.queueunit.5, hotkey.session.queueunit.6, hotkey.session.queueunit.7 With training structure selected. Add the 1st, 2nd, … unit shown to the training queue for all the selected structures
hotkey.session.highlightguarded with unit(s) selected Highlight the unit(s)/structure(s) guarded by the selection
hotkey.session.highlightguarding with unit(s)/structure(s) selected Highlight the unit(s) guarding the selection
highlightguarding.showstatusbars See all status bars (which would also show the building progress)
hotkey.summary Toggle summary window
hotkey.lobby Show the multiplayer lobby in a dialog window
hotkey.session.gui.diplomacy.toggle Toggle in-game diplomacy panel
hotkey.session.gui.barter.toggle Toggle in-game barter and trade panel
hotkey.session.gui.objectives.toggle Toggle in-game objectives panel
hotkey.session.gui.tutorial.toggle Toggle in-game tutorial panel
hotkey.structree Toggle structure tree panel
hotkey.civinfo Toggle civilization info panel
[font="sans-bold-14"]Modify mouse action[font="sans-14"]
Ctrl + Right Click on structure Garrison
J + Right Click on structure Repair
P + Right Click Patrol
Shift + Right Click Queue the move/build/gather/etc. order
Alt + Right Click Order one unit from the current selection to move/build/gather/etc. and unselect it. Used to quickly dispatch units with specific tasks
Shift + Left Click when training units Add units in batches (the batch size is 5 by default and can be changed in the options)
Shift + Left Click or Left Drag over unit on map Add unit to selection
Ctrl + Left Click or Left Drag over unit on map Remove unit from selection
Alt + Left Drag over units on map Only select military units
Alt + Y + Left Drag over units on map Only select non-military units
I + Left Drag over units on map Only select idle units
O + Left Drag over units on map Only select wounded units
Ctrl + Left Click on unit/group icon with multiple units selected Deselect
hotkey.session.garrison + Right Click on structure Garrison
hotkey.session.repair + Right Click on structure Repair
hotkey.session.patrol + Right Click Patrol
hotkey.session.queue + Right Click Queue the move/build/gather/etc. order
hotkey.session.orderone + Right Click Order one unit from the current selection to move/build/gather/etc. and unselect it. Used to quickly dispatch units with specific tasks
hotkey.session.batchtrain + Left Click when training units Add units in batches (the batch size is 5 by default and can be changed in the options)
hotkey.selection.add + Left Click or Left Drag over unit on map Add unit to selection
hotkey.selection.remove + Left Click or Left Drag over unit on map Remove unit from selection
hotkey.selection.militaryonly + Left Drag over units on map Only select military units
hotkey.selection.nonmilitaryonly + Left Drag over units on map Only select non-military units
hotkey.selection.idleonly + Left Drag over units on map Only select idle units
hotkey.selection.woundedonly + Left Drag over units on map Only select wounded units
Right Click with a structure(s) selected Set a rally point for units created/ungarrisoned from that structure
Ctrl + Right Click with unit(s) selected:
• If the cursor is over an own or allied structure Garrison
• If the cursor is over an enemy unit/structure Attack (instead of capture or gather)
• Otherwise Attack move (by default all enemy units and structures along the way are targeted)
Ctrl + Q + Right Click with unit(s) selected Attack move, only units along the way are targeted
Ctrl + Mouse Move near structures Align the new structure with an existing nearby structure
hotkey.session.garrison + Right Click with unit(s) selected - Garrison (If the cursor is over an own or allied structure)
hotkey.session.attack + Right Click with unit(s) selected - Attack (instead of capture or gather)
hotkey.session.attackmove + Right Click with unit(s) selected - Attack move (by default all enemy units and structures along the way are targeted)
hotkey.session.attackmoveUnit + Right Click with unit(s) selected - Attack move, only units along the way are targeted
hotkey.session.snaptoedges + Mouse Move near structures Align the new structure with an existing nearby structure
[font="sans-bold-14"]Overlays[font="sans-14"]
Alt + G Toggle the GUI
Alt + D Toggle developer overlay (with developer options)
Alt + Shift + W Toggle wireframe mode (press once to get wireframes overlaid over the textured models, twice to get just the wireframes colored by the textures, thrice to get back to normal textured mode)
Alt + Shift + S Toggle unit silhouettes (might give a small performance boost)
Alt + Z Toggle sky
Alt + X Toggle diplomacy colors
Alt + C Toggle attack range visualizations of selected units and structures
Alt + V Toggle aura range visualizations of selected units and structures
Alt + B Toggle heal range visualizations of selected units
hotkey.session.gui.toggle Toggle the GUI
hotkey.session.devcommands.toggle Toggle developer overlay (with developer options)
hotkey.wireframe Toggle wireframe mode (press once to get wireframes overlaid over the textured models, twice to get just the wireframes colored by the textures, thrice to get back to normal textured mode)
hotkey.silhouettes Toggle unit silhouettes (might give a small performance boost)
hotkey.showsky Toggle sky
hotkey.session.diplomacycolors Toggle diplomacy colors
hotkey.session.toggleattackrange Toggle attack range visualizations of selected units and structures
hotkey.session.toggleaurasrange Toggle aura range visualizations of selected units and structures
hotkey.session.togglehealrange Toggle heal range visualizations of selected units
[font="sans-bold-14"]Camera manipulation[font="sans-14"]
W or ↑ (Up) Pan screen up
S or ↓ (Down) Pan screen down
A or ← (Left) Pan screen left
D or → (Right) Pan screen right
Ctrl + W or Ctrl + ↑ (Up) Rotate camera to look upward
Ctrl + S or Ctrl + ↓ (Down) Rotate camera to look downward
Ctrl + A or Ctrl + ← (Left) Rotate camera clockwise around terrain
Ctrl + D or Ctrl + → (Right) Rotate camera counter-clockwise around terrain
Q Rotate camera clockwise around terrain
E Rotate camera counter-clockwise around terrain
Shift + Mouse Wheel Rotate Up Rotate camera clockwise around terrain
Shift + Mouse Wheel Rotate Down Rotate camera counter-clockwise around terrain
F Follow the selected unit (move the camera to stop following the unit)
R Reset camera zoom and orientation
= (Equals) or + (Plus) Zoom in (keep pressed for continuous zoom)
- (Hyphen) or (Minus) Zoom out (keep pressed for continuous zoom)
Middle Mouse Button Keep pressed and move the mouse to pan
hotkey.camera.up Pan screen up
hotkey.camera.down Pan screen down
hotkey.camera.left Pan screen left
hotkey.camera.right Pan screen right
hotkey.camera.rotate.up Rotate camera to look upward
hotkey.camera.rotate.down Rotate camera to look downward
hotkey.camera.rotate.cw Rotate camera clockwise around terrain
hotkey.camera.rotate.ccw Rotate camera counter-clockwise around terrain
hotkey.camera.rotate.wheel.cw Rotate camera clockwise around terrain
hotkey.camera.rotate.wheel.ccw Rotate camera counter-clockwise around terrain
hotkey.camera.follow Follow the selected unit (move the camera to stop following the unit)
hotkey.camera.reset Reset camera zoom and orientation
hotkey.camera.zoom.in, hotkey.camera.zoom.wheel.in Zoom in (keep pressed for continuous zoom)
hotkey.camera.zoom.out, hotkey.camera.zoom.wheel.out Zoom out (keep pressed for continuous zoom)
hotkey.camera.pan Keep pressed and move the mouse to pan
[font="sans-bold-14"]During Structure Placement[font="sans-14"]
\[ (Left Bracket) Rotate structure 15 degrees counter-clockwise
] (Right Bracket) Rotate structure 15 degrees clockwise
hotkey.session.rotate.ccw Rotate structure 15 degrees counter-clockwise
hotkey.session.rotate.cw Rotate structure 15 degrees clockwise
Left Drag Rotate structure using mouse (foundation will be placed on mouse release)
[font="sans-bold-14"]When loading a saved game[font="sans-14"]
Esc Cancel
Delete Delete the selected saved game, ask for confirmation
Shift + Delete Immediately delete the selected saved game, without asking for confirmation
hotkey.cancel Cancel
hotkey.session.savedgames.delete Delete the selected saved game, ask for confirmation
• With hotkey.session.savedgames.noconfirmation Don't ask for confirmation

View file

@ -0,0 +1,10 @@
function init()
{
let mainText = Engine.GetGUIObjectByName("mainText");
let text = Engine.TranslateLines(Engine.ReadFile("gui/manual/intro.txt"));
let hotkeys = Engine.GetHotkeyMap();
// Replace anything starting with 'hotkey.' with its hotkey.
mainText.caption = text.replace(/hotkey\.([a-z0-9_\.]+)/g, (_, k) => formatHotkeyCombinations(hotkeys[k]));
}

View file

@ -14,9 +14,7 @@
</object>
<object type="image" sprite="ModernFade" size="20 20 100%-20 100%-58">
<object name="mainText" type="text" style="ModernTextPanel">
<action on="Load">this.caption = Engine.TranslateLines(Engine.ReadFile("gui/manual/intro.txt"));</action>
</object>
<object name="mainText" type="text" style="ModernTextPanel" />
</object>
<object type="button" style="ModernButtonRed" tooltip_style="snToolTip" size="100%-408 100%-52 100%-218 100%-24" hotkey="cancel">

View file

@ -148,6 +148,13 @@ var g_MainMenuItems = [
fireConfigChangeHandlers);
}
},
{
"caption": translate("Hotkeys"),
"tooltip": translate("Adjust hotkeys."),
"onPress": () => {
Engine.PushGuiPage("hotkeys/page_hotkeys.xml");
}
},
{
"caption": translate("Language"),
"tooltip": translate("Choose the language of the game."),

View file

@ -174,6 +174,27 @@ MenuButtons.prototype.Options = class
}
};
MenuButtons.prototype.Hotkeys = class
{
constructor(button, pauseControl)
{
this.button = button;
this.button.caption = translate("Hotkeys");
this.pauseControl = pauseControl;
}
onPress()
{
closeOpenDialogs();
this.pauseControl.implicitPause();
Engine.PushGuiPage(
"hotkeys/page_hotkeys.xml",
{},
() => { resumeGame(); });
}
};
MenuButtons.prototype.Pause = class
{
constructor(button, pauseControl, playerViewControl)

View file

@ -151,21 +151,19 @@ InReaction CGUI::HandleEvent(const SDL_Event_* ev)
m_MousePos = CPos((float)ev->ev.button.x / g_GuiScale, (float)ev->ev.button.y / g_GuiScale);
}
// Allow the focused object to pre-empt regular GUI events.
if (GetFocusedObject())
ret = GetFocusedObject()->PreemptEvent(ev);
// Only one object can be hovered
IGUIObject* pNearest = nullptr;
// pNearest will after this point at the hovered object, possibly nullptr
IGUIObject* pNearest = FindObjectUnderMouse();
// TODO Gee: (2004-09-08) Big TODO, don't do the below if the SDL_Event is something like a keypress!
if (ret == IN_PASS)
{
PROFILE("mouse events");
// TODO Gee: Optimizations needed!
// these two recursive function are quite overhead heavy.
// pNearest will after this point at the hovered object, possibly nullptr
pNearest = FindObjectUnderMouse();
// Now we'll call UpdateMouseOver on *all* objects,
// we'll input the one hovered, and they will each
// update their own data and send messages accordingly
// we'll input the one hovered, and they will each
// update their own data and send messages accordingly
m_BaseObject.RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::UpdateMouseOver, static_cast<IGUIObject* const&>(pNearest));
if (ev->ev.type == SDL_MOUSEBUTTONDOWN)
@ -253,19 +251,13 @@ InReaction CGUI::HandleEvent(const SDL_Event_* ev)
if (ev->ev.type == SDL_MOUSEBUTTONDOWN || ev->ev.type == SDL_MOUSEBUTTONUP)
m_MousePos = oldMousePos;
// Handle keys for input boxes
if (GetFocusedObject())
// Let GUI items handle keys after everything else, e.g. for input boxes.
if (ret == IN_PASS && GetFocusedObject())
{
if ((ev->ev.type == SDL_KEYDOWN &&
ev->ev.key.keysym.sym != SDLK_ESCAPE &&
!g_keys[SDLK_LCTRL] && !g_keys[SDLK_RCTRL] &&
!g_keys[SDLK_LALT] && !g_keys[SDLK_RALT]) ||
ev->ev.type == SDL_HOTKEYDOWN ||
ev->ev.type == SDL_TEXTINPUT ||
ev->ev.type == SDL_TEXTEDITING)
{
ret = GetFocusedObject()->ManuallyHandleEvent(ev);
}
if (ev->ev.type == SDL_KEYUP || ev->ev.type == SDL_KEYDOWN ||
ev->ev.type == SDL_HOTKEYUP || ev->ev.type == SDL_HOTKEYDOWN ||
ev->ev.type == SDL_TEXTINPUT || ev->ev.type == SDL_TEXTEDITING)
ret = GetFocusedObject()->ManuallyHandleKeys(ev);
// else will return IN_PASS because we never used the button.
}
@ -274,6 +266,7 @@ InReaction CGUI::HandleEvent(const SDL_Event_* ev)
void CGUI::TickObjects()
{
m_BaseObject.RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::Tick);
SendEventToAll(EventNameTick);
m_Tooltip.Update(FindObjectUnderMouse(), m_MousePos, *this);
}

View file

@ -21,6 +21,7 @@
#include "gui/ObjectTypes/CChart.h"
#include "gui/ObjectTypes/CCheckBox.h"
#include "gui/ObjectTypes/CDropDown.h"
#include "gui/ObjectTypes/CHotkeyPicker.h"
#include "gui/ObjectTypes/CImage.h"
#include "gui/ObjectTypes/CInput.h"
#include "gui/ObjectTypes/CList.h"
@ -39,6 +40,7 @@ void CGUI::AddObjectTypes()
AddObjectType("checkbox", &CCheckBox::ConstructObject);
AddObjectType("dropdown", &CDropDown::ConstructObject);
AddObjectType("empty", &CGUIDummyObject::ConstructObject);
AddObjectType("hotkeypicker", &CHotkeyPicker::ConstructObject);
AddObjectType("image", &CImage::ConstructObject);
AddObjectType("input", &CInput::ConstructObject);
AddObjectType("list", &CList::ConstructObject);

View file

@ -255,6 +255,12 @@ protected:
//@{
public:
/**
* Called on every GUI tick unless the object or one of its parent is hidden/ghost.
*/
virtual void Tick() {};
/**
* This function is called with different messages
* for instance when the mouse enters the object.
@ -290,7 +296,17 @@ protected:
virtual void Draw() = 0;
/**
* Some objects need to handle the SDL_Event_ manually.
* Some objects need to be able to pre-emptively process SDL_Event_.
*
* Only the object with focus will have this function called.
*
* Returns either IN_PASS or IN_HANDLED. If IN_HANDLED, then
* the event won't be passed on and processed by other handlers.
*/
virtual InReaction PreemptEvent(const SDL_Event_* UNUSED(ev)) { return IN_PASS; }
/**
* Some objects need to handle the text-related SDL_Event_ manually.
* For instance the input box.
*
* Only the object with focus will have this function called.
@ -299,7 +315,7 @@ protected:
* the key won't be passed on and processed by other handlers.
* This is used for keys that the GUI uses.
*/
virtual InReaction ManuallyHandleEvent(const SDL_Event_* UNUSED(ev)) { return IN_PASS; }
virtual InReaction ManuallyHandleKeys(const SDL_Event_* UNUSED(ev)) { return IN_PASS; }
/**
* Loads a style.

View file

@ -273,7 +273,7 @@ void CDropDown::HandleMessage(SGUIMessage& Message)
SetupText();
}
InReaction CDropDown::ManuallyHandleEvent(const SDL_Event_* ev)
InReaction CDropDown::ManuallyHandleKeys(const SDL_Event_* ev)
{
InReaction result = IN_PASS;
bool update_highlight = false;
@ -298,7 +298,7 @@ InReaction CDropDown::ManuallyHandleEvent(const SDL_Event_* ev)
if (!m_Open)
return IN_PASS;
// Set current selected item to highlighted, before
// then really processing these in CList::ManuallyHandleEvent()
// then really processing these in CList::ManuallyHandleKeys()
SetSetting<i32>("selected", m_ElementHighlight, true);
update_highlight = true;
break;
@ -356,7 +356,7 @@ InReaction CDropDown::ManuallyHandleEvent(const SDL_Event_* ev)
}
}
if (CList::ManuallyHandleEvent(ev) == IN_HANDLED)
if (CList::ManuallyHandleKeys(ev) == IN_HANDLED)
result = IN_HANDLED;
if (update_highlight)

View file

@ -58,7 +58,7 @@ public:
/**
* Handle events manually to catch keyboard inputting.
*/
virtual InReaction ManuallyHandleEvent(const SDL_Event_* ev);
virtual InReaction ManuallyHandleKeys(const SDL_Event_* ev);
/**
* Draws the Button

View file

@ -0,0 +1,191 @@
/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "CHotkeyPicker.h"
#include "lib/timer.h"
#include "ps/Hotkey.h"
#include "ps/KeyName.h"
#include "scriptinterface/ScriptConversions.h"
const CStr CHotkeyPicker::EventNameCombination = "Combination";
const CStr CHotkeyPicker::EventNameKeyChange = "KeyChange";
// Don't send the scancode, JS doesn't care.
template<> void ScriptInterface::ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret, const CHotkeyPicker::Key& val)
{
ScriptInterface::ToJSVal(rq, ret, val.scancodeName);
}
// Unused, but JSVAL_VECTOR requires it.
template<> bool ScriptInterface::FromJSVal(const ScriptRequest&, const JS::HandleValue, CHotkeyPicker::Key&)
{
LOGWARNING("FromJSVal<CHotkeyPicker>: Not implemented");
return false;
}
JSVAL_VECTOR(CHotkeyPicker::Key);
CHotkeyPicker::CHotkeyPicker(CGUI& pGUI) : IGUIObject(pGUI), m_TimeToCombination(1.f)
{
RegisterSetting("time_to_combination", m_TimeToCombination);
// 8 keys at the same time is probably more than we'll ever need.
m_KeysPressed.reserve(8);
}
CHotkeyPicker::~CHotkeyPicker()
{
}
void CHotkeyPicker::FireEvent(const CStr& event)
{
ScriptRequest rq(*m_pGUI.GetScriptInterface());
JS::AutoValueArray<1> args(rq.cx);
JS::RootedValue keys(rq.cx);
m_pGUI.GetScriptInterface()->ToJSVal(rq, &keys, m_KeysPressed);
args[0].set(keys);
ScriptEvent(event, args);
}
void CHotkeyPicker::Tick()
{
if (m_KeysPressed.size() == 0)
return;
double time = timer_Time();
if (time - m_LastKeyChange < m_TimeToCombination)
return;
FireEvent(EventNameCombination);
return;
}
void CHotkeyPicker::HandleMessage(SGUIMessage& Message)
{
IGUIObject::HandleMessage(Message);
switch (Message.type)
{
case GUIM_GOT_FOCUS:
case GUIM_LOST_FOCUS:
{
m_KeysPressed.clear();
m_LastKeyChange = timer_Time();
break;
}
default:
break;
}
}
InReaction CHotkeyPicker::PreemptEvent(const SDL_Event_* ev)
{
switch (ev->ev.type)
{
// Handle the same mouse events that hotkeys handle
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
case SDL_MOUSEWHEEL:
{
SDL_Scancode scancode;
if (ev->ev.type != SDL_MOUSEWHEEL)
{
// Wait a little bit -> this gets triggered when clicking on a button,
// but after the button click is processed, thus immediately triggering...
if (timer_Time()-m_LastKeyChange < 0.2)
return IN_HANDLED;
// This is from hotkeyHandler - not sure what it does in all honesty.
if(ev->ev.button.button >= SDL_BUTTON_X1)
scancode = static_cast<SDL_Scancode>(MOUSE_BASE + (int)ev->ev.button.button + 2);
else
scancode = static_cast<SDL_Scancode>(MOUSE_BASE + (int)ev->ev.button.button);
}
else
{
if (ev->ev.wheel.y > 0)
scancode = static_cast<SDL_Scancode>(MOUSE_WHEELUP);
else if (ev->ev.wheel.y < 0)
scancode = static_cast<SDL_Scancode>(MOUSE_WHEELDOWN);
else if (ev->ev.wheel.x > 0)
scancode = static_cast<SDL_Scancode>(MOUSE_X2);
else if (ev->ev.wheel.x < 0)
scancode = static_cast<SDL_Scancode>(MOUSE_X1);
else
return IN_HANDLED;
}
// Don't handle keys and mouse together except for modifiers.
std::remove_if(m_KeysPressed.begin(), m_KeysPressed.end(), [](const Key& k) {
return static_cast<int>(k.code) < UNIFIED_SHIFT || static_cast<int>(k.code) >= UNIFIED_LAST; } );
m_KeysPressed.emplace_back(Key{scancode, FindScancodeName(scancode)});
// For mouse events, assume we immediately want to return.
FireEvent(EventNameCombination);
return IN_HANDLED;
}
case SDL_KEYDOWN:
case SDL_KEYUP:
{
SDL_Scancode scancode = ev->ev.key.keysym.scancode;
// Don't handle caps-lock, it doesn't really work in-game and it's a weird hotkey.
if (scancode == SDL_SCANCODE_CAPSLOCK)
return IN_PASS;
if (scancode == SDL_SCANCODE_LSHIFT || scancode == SDL_SCANCODE_RSHIFT)
scancode = static_cast<SDL_Scancode>(UNIFIED_SHIFT);
else if (scancode == SDL_SCANCODE_LCTRL || scancode == SDL_SCANCODE_RCTRL)
scancode = static_cast<SDL_Scancode>(UNIFIED_CTRL);
else if (scancode == SDL_SCANCODE_LALT || scancode == SDL_SCANCODE_RALT)
scancode = static_cast<SDL_Scancode>(UNIFIED_ALT);
else if (scancode == SDL_SCANCODE_LGUI || scancode == SDL_SCANCODE_RGUI)
scancode = static_cast<SDL_Scancode>(UNIFIED_SUPER);
if (ev->ev.type == SDL_KEYDOWN)
{
std::vector<Key>::const_iterator it = \
std::find_if(m_KeysPressed.begin(), m_KeysPressed.end(), [&scancode](Key& k) { return k.code == scancode; });
// Can happen if multiple keys are mapped the same.
if (it != m_KeysPressed.end())
return IN_HANDLED;
m_KeysPressed.emplace_back(Key{scancode, FindScancodeName(scancode)});
}
else
{
std::vector<Key>::const_iterator it = \
std::find_if(m_KeysPressed.begin(), m_KeysPressed.end(), [&scancode](Key& k) { return k.code == scancode; });
// Might happen if a key was down before this object is created.
if (it == m_KeysPressed.end())
return IN_HANDLED;
m_KeysPressed.erase(it);
}
FireEvent(EventNameKeyChange);
// Register after-JS in case this takes a while (probably not but it doesn't hurt).
m_LastKeyChange = timer_Time();
return IN_HANDLED;
}
default:
{
return IN_PASS;
}
}
}

View file

@ -0,0 +1,78 @@
/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_CHOTKEYPICKER
#define INCLUDED_CHOTKEYPICKER
#include "lib/external_libraries/libsdl.h"
#include "ps/CStr.h"
class ScriptInterface;
/**
* When in focus, returns all currently pressed keys.
* After a set time without changes, it will trigger a "combination" event.
*
* Used to create new hotkey combinations in-game. Mostly custom.
* This object does not draw anything.
*
* NB: because of how input is handled, mouse clicks
*/
class CHotkeyPicker : public IGUIObject
{
GUI_OBJECT(CHotkeyPicker)
friend class ScriptInterface;
public:
CHotkeyPicker(CGUI& pGUI);
virtual ~CHotkeyPicker();
// Do nothing.
virtual void Draw() {};
// Checks if the timer has passed and we need to fire a "combination" event.
virtual void Tick();
// React to blur/focus.
virtual void HandleMessage(SGUIMessage& Message);
// Pre-empt events: this is our sole purpose.
virtual InReaction PreemptEvent(const SDL_Event_* ev);
protected:
// Fire an event with m_KeysPressed as argument.
void FireEvent(const CStr& event);
// Time without changes until a "combination" event is sent.
float m_TimeToCombination;
// Time of the last registered key change.
double m_LastKeyChange;
// Keep track of which keys we are pressing, and precompute their name for JS code.
struct Key
{
// The scancode is used for fast comparisons.
SDL_Scancode code;
// This is the name ultimately stored in the config file.
CStr scancodeName;
};
std::vector<Key> m_KeysPressed;
static const CStr EventNameCombination;
static const CStr EventNameKeyChange;
};
#endif // INCLUDED_CHOTKEYPICKER

View file

@ -114,7 +114,7 @@ void CInput::ClearComposedText()
m_iComposedPos = 0;
}
InReaction CInput::ManuallyHandleEvent(const SDL_Event_* ev)
InReaction CInput::ManuallyHandleKeys(const SDL_Event_* ev)
{
ENSURE(m_iBufferPos != -1);
@ -226,6 +226,11 @@ InReaction CInput::ManuallyHandleEvent(const SDL_Event_* ev)
// pointer and edit that.
SDL_Keycode keyCode = ev->ev.key.keysym.sym;
// Escape is treated specially to let players close out windows even if an input is in focus.
// TODO: there is maybe a better way to only handle text-like keys here?
if (keyCode == SDLK_ESCAPE)
return IN_PASS;
ManuallyImmutableHandleKeyDownEvent(keyCode);
ManuallyMutableHandleKeyDownEvent(keyCode);
@ -1117,7 +1122,7 @@ void CInput::HandleMessage(SGUIMessage& Message)
evt.ev.edit.length = 0;
evt.ev.edit.start = 0;
evt.ev.edit.text[0] = 0;
ManuallyHandleEvent(&evt);
ManuallyHandleKeys(&evt);
}
SDL_StopTextInput();

View file

@ -64,20 +64,20 @@ protected:
/**
* Handle events manually to catch keyboard inputting.
*/
virtual InReaction ManuallyHandleEvent(const SDL_Event_* ev);
virtual InReaction ManuallyHandleKeys(const SDL_Event_* ev);
/**
* Handle events manually to catch keys which change the text.
*/
* Handle events manually to catch keys which change the text.
*/
virtual void ManuallyMutableHandleKeyDownEvent(const SDL_Keycode keyCode);
/**
* Handle events manually to catch keys which don't change the text.
*/
* Handle events manually to catch keys which don't change the text.
*/
virtual void ManuallyImmutableHandleKeyDownEvent(const SDL_Keycode keyCode);
/**
* Handle hotkey events (called by ManuallyHandleEvent)
* Handle hotkey events (called by ManuallyHandleKeys)
*/
virtual InReaction ManuallyHandleHotkeyEvent(const SDL_Event_* ev);

View file

@ -251,7 +251,7 @@ void CList::HandleMessage(SGUIMessage& Message)
IGUITextOwner::HandleMessage(Message);
}
InReaction CList::ManuallyHandleEvent(const SDL_Event_* ev)
InReaction CList::ManuallyHandleKeys(const SDL_Event_* ev)
{
InReaction result = IN_PASS;

View file

@ -73,7 +73,7 @@ protected:
/**
* Handle events manually to catch keyboard inputting.
*/
virtual InReaction ManuallyHandleEvent(const SDL_Event_* ev);
virtual InReaction ManuallyHandleKeys(const SDL_Event_* ev);
/**
* Draws the List box

View file

@ -30,6 +30,7 @@
#include "ps/scripting/JSInterface_Console.h"
#include "ps/scripting/JSInterface_Debug.h"
#include "ps/scripting/JSInterface_Game.h"
#include "ps/scripting/JSInterface_Hotkey.h"
#include "ps/scripting/JSInterface_Main.h"
#include "ps/scripting/JSInterface_Mod.h"
#include "ps/scripting/JSInterface_ModIo.h"
@ -59,6 +60,7 @@ void GuiScriptingInit(ScriptInterface& scriptInterface)
JSI_GUIManager::RegisterScriptFunctions(scriptInterface);
JSI_Game::RegisterScriptFunctions(scriptInterface);
JSI_GameView::RegisterScriptFunctions(scriptInterface);
JSI_Hotkey::RegisterScriptFunctions(scriptInterface);
JSI_L10n::RegisterScriptFunctions(scriptInterface);
JSI_Lobby::RegisterScriptFunctions(scriptInterface);
JSI_Main::RegisterScriptFunctions(scriptInterface);

View file

@ -72,7 +72,7 @@ public:
// Press 'a'.
SDL_Event_ hotkeyNotification;
hotkeyNotification.ev.type = SDL_KEYDOWN;
hotkeyNotification.ev.key.keysym.sym = SDLK_a;
hotkeyNotification.ev.key.keysym.scancode = SDL_SCANCODE_A;
hotkeyNotification.ev.key.repeat = 0;
// Init input and poll the event.

View file

@ -210,6 +210,18 @@ void CConfigDB::SetValueBool(EConfigNamespace ns, const CStr& name, const bool v
SetValueString(ns, name, valueString);
}
void CConfigDB::SetValueList(EConfigNamespace ns, const CStr& name, std::vector<CStr> values)
{
CHECK_NS(;);
std::lock_guard<std::recursive_mutex> s(cfgdb_mutex);
TConfigMap::iterator it = m_Map[ns].find(name);
if (it == m_Map[ns].end())
it = m_Map[ns].insert(m_Map[ns].begin(), make_pair(name, CConfigValueSet(1)));
it->second = values;
}
void CConfigDB::RemoveValue(EConfigNamespace ns, const CStr& name)
{
CHECK_NS(;);

View file

@ -110,6 +110,8 @@ public:
void SetValueBool(EConfigNamespace ns, const CStr& name, const bool value);
void SetValueList(EConfigNamespace ns, const CStr& name, std::vector<CStr> values);
/**
* Remove a config value in the specified namespace.
*/

View file

@ -20,6 +20,7 @@
#include <boost/tokenizer.hpp>
#include "lib/external_libraries/libsdl.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/CStr.h"
@ -29,29 +30,11 @@
static bool unified[UNIFIED_LAST - UNIFIED_SHIFT];
struct SKey
{
SDL_Keycode code; // keycode or MOUSE_ or UNIFIED_ value
bool negated; // whether the key must be pressed (false) or unpressed (true)
};
std::unordered_map<int, KeyMapping> g_HotkeyMap;
std::unordered_map<std::string, bool> g_HotkeyStatus;
// Hotkey data associated with an externally-specified 'primary' keycode
struct SHotkeyMapping
{
CStr name; // name of the hotkey
bool negated; // whether the primary key must be pressed (false) or unpressed (true)
std::vector<SKey> requires; // list of non-primary keys that must also be active
};
typedef std::vector<SHotkeyMapping> KeyMapping;
// A mapping of keycodes onto the hotkeys that are associated with that key.
// (A hotkey triggered by a combination of multiple keys will be in this map
// multiple times.)
static std::map<int, KeyMapping> g_HotkeyMap;
// The current pressed status of hotkeys
std::map<std::string, bool> g_HotkeyStatus;
static_assert(std::is_integral<std::underlying_type<SDL_Scancode>::type>::value, "SDL_Scancode is not an integral enum.");
static_assert(SDL_USEREVENT_ == SDL_USEREVENT, "SDL_USEREVENT_ is not the same type as the real SDL_USEREVENT");
// Look up each key binding in the config file and set the mappings for
// all key combinations that trigger it.
@ -74,16 +57,14 @@ static void LoadConfigBindings()
for (tokenizer::iterator it = tok.begin(); it != tok.end(); ++it)
{
// Attempt decode as key name
int mapping = FindKeyCode(*it);
if (!mapping)
mapping = SDL_GetKeyFromName(it->c_str());
if (!mapping)
SDL_Scancode scancode = FindScancode(it->c_str());
if (!scancode)
{
LOGWARNING("Hotkey mapping used invalid key '%s'", hotkey.c_str());
continue;
}
SKey key = { (SDL_Keycode)mapping, false };
SKey key = { scancode, false };
keyCombination.push_back(key);
}
@ -107,8 +88,6 @@ static void LoadConfigBindings()
void LoadHotkeys()
{
InitKeyNameMap();
LoadConfigBindings();
// Set up the state of the hotkeys given no key is down.
@ -146,7 +125,7 @@ bool isNegated(const SKey& key)
else if ((int)key.code < MOUSE_LAST && (int)key.code > MOUSE_BASE && g_mouse_buttons[key.code - MOUSE_BASE] == key.negated)
return false;
// Modifier keycodes are between the normal keys and the mouse 'keys'
else if ((int)key.code < UNIFIED_LAST && (int)key.code > SDL_SCANCODE_TO_KEYCODE(SDL_NUM_SCANCODES) && unified[key.code - UNIFIED_SHIFT] == key.negated)
else if ((int)key.code < UNIFIED_LAST && (int)key.code > SDL_NUM_SCANCODES && unified[key.code - UNIFIED_SHIFT] == key.negated)
return false;
else
return true;
@ -163,13 +142,13 @@ InReaction HotkeyStateChange(const SDL_Event_* ev)
InReaction HotkeyInputHandler(const SDL_Event_* ev)
{
int keycode = 0;
int scancode = SDL_SCANCODE_UNKNOWN;
switch(ev->ev.type)
{
case SDL_KEYDOWN:
case SDL_KEYUP:
keycode = (int)ev->ev.key.keysym.sym;
scancode = ev->ev.key.keysym.scancode;
break;
case SDL_MOUSEBUTTONDOWN:
@ -177,30 +156,30 @@ InReaction HotkeyInputHandler(const SDL_Event_* ev)
// Mousewheel events are no longer buttons, but we want to maintain the order
// expected by g_mouse_buttons for compatibility
if (ev->ev.button.button >= SDL_BUTTON_X1)
keycode = MOUSE_BASE + (int)ev->ev.button.button + 2;
scancode = MOUSE_BASE + (int)ev->ev.button.button + 2;
else
keycode = MOUSE_BASE + (int)ev->ev.button.button;
scancode = MOUSE_BASE + (int)ev->ev.button.button;
break;
case SDL_MOUSEWHEEL:
if (ev->ev.wheel.y > 0)
{
keycode = MOUSE_WHEELUP;
scancode = MOUSE_WHEELUP;
break;
}
else if (ev->ev.wheel.y < 0)
{
keycode = MOUSE_WHEELDOWN;
scancode = MOUSE_WHEELDOWN;
break;
}
else if (ev->ev.wheel.x > 0)
{
keycode = MOUSE_X2;
scancode = MOUSE_X2;
break;
}
else if (ev->ev.wheel.x < 0)
{
keycode = MOUSE_X1;
scancode = MOUSE_X1;
break;
}
return IN_PASS;
@ -219,33 +198,33 @@ InReaction HotkeyInputHandler(const SDL_Event_* ev)
if (phantom.ev.type == SDL_KEYDOWN)
phantom.ev.key.repeat = ev->ev.type == SDL_KEYDOWN ? ev->ev.key.repeat : 0;
if ((keycode == SDLK_LSHIFT) || (keycode == SDLK_RSHIFT))
if (scancode == SDL_SCANCODE_LSHIFT || scancode == SDL_SCANCODE_RSHIFT)
{
phantom.ev.key.keysym.sym = (SDL_Keycode)UNIFIED_SHIFT;
phantom.ev.key.keysym.scancode = static_cast<SDL_Scancode>(UNIFIED_SHIFT);
unified[0] = (phantom.ev.type == SDL_KEYDOWN);
HotkeyInputHandler(&phantom);
}
else if ((keycode == SDLK_LCTRL) || (keycode == SDLK_RCTRL))
else if (scancode == SDL_SCANCODE_LCTRL || scancode == SDL_SCANCODE_RCTRL)
{
phantom.ev.key.keysym.sym = (SDL_Keycode)UNIFIED_CTRL;
phantom.ev.key.keysym.scancode = static_cast<SDL_Scancode>(UNIFIED_CTRL);
unified[1] = (phantom.ev.type == SDL_KEYDOWN);
HotkeyInputHandler(&phantom);
}
else if ((keycode == SDLK_LALT) || (keycode == SDLK_RALT))
else if (scancode == SDL_SCANCODE_LALT || scancode == SDL_SCANCODE_RALT)
{
phantom.ev.key.keysym.sym = (SDL_Keycode)UNIFIED_ALT;
phantom.ev.key.keysym.scancode = static_cast<SDL_Scancode>(UNIFIED_ALT);
unified[2] = (phantom.ev.type == SDL_KEYDOWN);
HotkeyInputHandler(&phantom);
}
else if ((keycode == SDLK_LGUI) || (keycode == SDLK_RGUI))
else if (scancode == SDL_SCANCODE_LGUI || scancode == SDL_SCANCODE_RGUI)
{
phantom.ev.key.keysym.sym = (SDL_Keycode)UNIFIED_SUPER;
phantom.ev.key.keysym.scancode = static_cast<SDL_Scancode>(UNIFIED_SUPER);
unified[3] = (phantom.ev.type == SDL_KEYDOWN);
HotkeyInputHandler(&phantom);
}
// Check whether we have any hotkeys registered for this particular keycode
if (g_HotkeyMap.find(keycode) == g_HotkeyMap.end())
if (g_HotkeyMap.find(scancode) == g_HotkeyMap.end())
return (IN_PASS);
// Inhibit the dispatch of hotkey events caused by real keys (not fake mouse button
@ -253,7 +232,7 @@ InReaction HotkeyInputHandler(const SDL_Event_* ev)
bool consoleCapture = false;
if (g_Console && g_Console->IsActive() && keycode < SDL_SCANCODE_TO_KEYCODE(SDL_NUM_SCANCODES))
if (g_Console && g_Console->IsActive() && scancode < SDL_NUM_SCANCODES)
consoleCapture = true;
// Here's an interesting bit:
@ -273,7 +252,7 @@ InReaction HotkeyInputHandler(const SDL_Event_* ev)
std::vector<const char*> closestMapNames;
size_t closestMapMatch = 0;
for (const SHotkeyMapping& hotkey : g_HotkeyMap[keycode])
for (const SHotkeyMapping& hotkey : g_HotkeyMap[scancode])
{
// If a key has been pressed, and this event triggers on its release, skip it.
// Similarly, if the key's been released and the event triggers on a keypress, skip it.
@ -329,7 +308,7 @@ InReaction HotkeyInputHandler(const SDL_Event_* ev)
// -- KEYUP SECTION --
for (const SHotkeyMapping& hotkey : g_HotkeyMap[keycode])
for (const SHotkeyMapping& hotkey : g_HotkeyMap[scancode])
{
// If it's a keydown event, won't cause HotKeyUps in anything that doesn't
// use this key negated => skip them

View file

@ -33,15 +33,45 @@
#include "CStr.h"
#include "lib/input.h"
#include "lib/external_libraries/libsdl.h" // see note below
// note: we need the real SDL header - it defines SDL_USEREVENT, which is
// required for our HOTKEY event type definition. this is OK since
// hotkey.h is not included from any headers.
#include <unordered_map>
const uint SDL_HOTKEYPRESS = SDL_USEREVENT;
const uint SDL_HOTKEYDOWN = SDL_USEREVENT + 1;
const uint SDL_HOTKEYUP = SDL_USEREVENT + 2;
// SDL_Scancode is an enum, we'll use an explicit int to avoid including SDL in this header.
using SDL_Scancode_ = int;
// 0x8000 is SDL_USEREVENT, this is static_asserted in Hotkey.cpp
// We do this to avoid including SDL in this header.
const uint SDL_USEREVENT_ = 0x8000;
const uint SDL_HOTKEYPRESS = SDL_USEREVENT_;
const uint SDL_HOTKEYDOWN = SDL_USEREVENT_ + 1;
const uint SDL_HOTKEYUP = SDL_USEREVENT_ + 2;
struct SKey
{
SDL_Scancode_ code; // scancode or MOUSE_ or UNIFIED_ value
bool negated; // whether the key must be pressed (false) or unpressed (true)
bool operator<(const SKey& o) const { return code < o.code && negated < o.negated; }
bool operator==(const SKey& o) const { return code == o.code && negated == o.negated; }
};
// Hotkey data associated with an externally-specified 'primary' keycode
struct SHotkeyMapping
{
CStr name; // name of the hotkey
bool negated; // whether the primary key must be pressed (false) or unpressed (true)
std::vector<SKey> requires; // list of non-primary keys that must also be active
};
typedef std::vector<SHotkeyMapping> KeyMapping;
// A mapping of scancodes onto the hotkeys that are associated with that key.
// (A hotkey triggered by a combination of multiple keys will be in this map
// multiple times.)
extern std::unordered_map<SDL_Scancode_, KeyMapping> g_HotkeyMap;
// The current pressed status of hotkeys
extern std::unordered_map<std::string, bool> g_HotkeyStatus;
extern void LoadHotkeys();
extern void UnloadHotkeys();

View file

@ -24,215 +24,198 @@
#include "lib/external_libraries/libsdl.h"
#include "ps/CStr.h"
#include <map>
#include <unordered_map>
static std::map<CStr,int> keymap;
// Some scancodes <-> names that SDL doesn't recognise.
// Those are tested first so they override SDL defaults (useful for UNIFIED keys).
static const std::unordered_map<int, std::vector<CStr>> scancodemap {{
{ SDL_SCANCODE_DOWN, { "DownArrow" } },
{ SDL_SCANCODE_UP, { "UpArrow" } },
{ SDL_SCANCODE_LEFT, { "LeftArrow" } },
{ SDL_SCANCODE_RIGHT, { "RightArrow" } },
struct SKeycodeMapping
{ SDL_SCANCODE_EQUALS, { "Plus" } },
{ SDL_SCANCODE_MINUS, { "Minus" } },
{ SDL_SCANCODE_KP_ENTER, { "NumEnter" } },
{ SDL_SCANCODE_KP_DIVIDE, { "NumDivide" } },
{ SDL_SCANCODE_KP_MULTIPLY, { "NumMultiply" } },
{ SDL_SCANCODE_KP_EQUALS, { "NumEquals" } },
{ SDL_SCANCODE_KP_PERIOD, { "NumDecimal" } },
{ SDL_SCANCODE_KP_PLUS, { "NumPlus" } },
{ SDL_SCANCODE_KP_MINUS, { "NumMinus" } },
{ SDL_SCANCODE_KP_0, { "Num0" } },
{ SDL_SCANCODE_KP_1, { "Num1" } },
{ SDL_SCANCODE_KP_2, { "Num2" } },
{ SDL_SCANCODE_KP_3, { "Num3" } },
{ SDL_SCANCODE_KP_4, { "Num4" } },
{ SDL_SCANCODE_KP_5, { "Num5" } },
{ SDL_SCANCODE_KP_6, { "Num6" } },
{ SDL_SCANCODE_KP_7, { "Num7" } },
{ SDL_SCANCODE_KP_8, { "Num8" } },
{ SDL_SCANCODE_KP_9, { "Num9" } },
{ SDL_SCANCODE_COMMA, { "Comma" } },
{ SDL_SCANCODE_PERIOD, { "Period" } },
{ SDL_SCANCODE_APOSTROPHE, { "Quote" } },
{ SDL_SCANCODE_SEMICOLON, { "Semicolon" } },
{ SDL_SCANCODE_GRAVE, { "Backquote" } },
{ SDL_SCANCODE_LEFTBRACKET, { "LeftBracket" } },
{ SDL_SCANCODE_RIGHTBRACKET, { "RightBracket" } },
{ SDL_SCANCODE_BACKSLASH, { "Backslash" } },
{ SDL_SCANCODE_SLASH, { "Slash" } },
{ SDL_SCANCODE_RETURN, { "Enter" } },
{ SDL_SCANCODE_ESCAPE, { "Esc" } },
{ SDL_SCANCODE_PAUSE, { "Break" } },
{ SDL_SCANCODE_DELETE, { "Del" } },
{ MOUSE_LEFT, { "MouseLeft" } },
{ MOUSE_RIGHT, { "MouseRight" } },
{ MOUSE_MIDDLE, { "MouseMiddle" } },
{ MOUSE_WHEELUP, { "WheelUp" } },
{ MOUSE_WHEELDOWN, { "WheelDown" } },
{ MOUSE_X1, { "WheelLeft", "MouseX1" } },
{ MOUSE_X2, { "WheelRight", "MouseX2" } },
{ UNIFIED_SHIFT, { "Shift", "Left Shift", "Right Shift" } },
{ UNIFIED_CTRL, { "Ctrl", "Left Ctrl", "Right Ctrl" } },
{ UNIFIED_ALT, { "Alt", "Left Alt", "Right Alt" } },
{ UNIFIED_SUPER, { "Super", "Left Gui", "Right Gui" } },
}};
SDL_Scancode FindScancode(const CStr& keyname)
{
int keycode;
const char* keyname;
const char* altkeyname;
};
// Find (ignoring case) a corresponding scancode, if one exists.
std::unordered_map<int, std::vector<CStr>>::const_iterator it =
std::find_if(scancodemap.begin(), scancodemap.end(), [&keyname](const std::pair<int, std::vector<CStr>>& names) {
return std::find_if(names.second.begin(), names.second.end(), [&keyname](const CStr& t) {
return t.LowerCase() == keyname.LowerCase();
})!= names.second.end();
});
// You can use either key name in the config file...
if (it != scancodemap.end())
return static_cast<SDL_Scancode>(it->first);
static const SKeycodeMapping keycodeMapping[] =
SDL_Scancode code = SDL_GetScancodeFromName(keyname.c_str());
if (code != SDL_SCANCODE_UNKNOWN)
return code;
// Parse SYM_XX codes, see below.
if (keyname.size() > 4 && keyname.Left(4) == "SYM_")
return static_cast<SDL_Scancode>(CStr(keyname.substr(4)).ToInt());
return SDL_SCANCODE_UNKNOWN;
}
CStr FindScancodeName(SDL_Scancode scancode)
{
/* Just a tad friendlier than SDL_GetKeyName's name */
{ SDLK_BACKSPACE, "Backspace", "BkSp" },
{ SDLK_TAB, "Tab", 0 },
{ SDLK_CLEAR, "Clear", 0 }, // ?
{ SDLK_RETURN, "Return", "Ret" },
{ SDLK_PAUSE, "Pause", 0 }, // ?
{ SDLK_ESCAPE, "Escape", "Esc" },
{ SDLK_SPACE, "Space", "Spc" },
{ SDLK_EXCLAIM, "!", "Exclaim" },
{ SDLK_QUOTEDBL, "\"", "DoubleQuote" },
{ SDLK_HASH, "#", "Hash" },
{ SDLK_DOLLAR, "$", "Dollar" },
{ SDLK_AMPERSAND, "&", "Ampersand" },
{ SDLK_QUOTE, "'", "SingleQuote" },
{ SDLK_LEFTPAREN, "(", "LeftParen" },
{ SDLK_RIGHTPAREN, ")", "RightParen" },
{ SDLK_ASTERISK, "*", "Asterisk" },
{ SDLK_PLUS, "+", "Plus" },
{ SDLK_COMMA, ",", "Comma" },
{ SDLK_MINUS, "-", "Minus" },
{ SDLK_PERIOD, ".", "Period" },
{ SDLK_SLASH, "/", "ForwardSlash" },
{ SDLK_0, "0", 0 },
{ SDLK_1, "1", 0 },
{ SDLK_2, "2", 0 },
{ SDLK_3, "3", 0 },
{ SDLK_4, "4", 0 },
{ SDLK_5, "5", 0 },
{ SDLK_6, "6", 0 },
{ SDLK_7, "7", 0 },
{ SDLK_8, "8", 0 },
{ SDLK_9, "9", 0 },
{ SDLK_COLON, ":", "Colon" },
{ SDLK_SEMICOLON, ";", "Semicolon" },
{ SDLK_LESS, "<", "LessThan" },
{ SDLK_EQUALS, "=", "Equals" },
{ SDLK_GREATER, ">", "GreaterThan" },
{ SDLK_QUESTION, "?", "Question" },
{ SDLK_AT, "@", "At" },
{ SDLK_LEFTBRACKET, "[", "LeftBracket" },
{ SDLK_BACKSLASH, "\\", "BackSlash" },
{ SDLK_RIGHTBRACKET, "]", "RightBracket" },
{ SDLK_CARET, "^", "Caret", },
{ SDLK_UNDERSCORE, "_", "Underscore" },
{ SDLK_BACKQUOTE, "`", "BackQuote" },
{ SDLK_a, "A", 0 },
{ SDLK_b, "B", 0 },
{ SDLK_c, "C", 0 },
{ SDLK_d, "D", 0 },
{ SDLK_e, "E", 0 },
{ SDLK_f, "F", 0 },
{ SDLK_g, "G", 0 },
{ SDLK_h, "H", 0 },
{ SDLK_i, "I", 0 },
{ SDLK_j, "J", 0 },
{ SDLK_k, "K", 0 },
{ SDLK_l, "L", 0 },
{ SDLK_m, "M", 0 },
{ SDLK_n, "N", 0 },
{ SDLK_o, "O", 0 },
{ SDLK_p, "P", 0 },
{ SDLK_q, "Q", 0 },
{ SDLK_r, "R", 0 },
{ SDLK_s, "S", 0 },
{ SDLK_t, "T", 0 },
{ SDLK_u, "U", 0 },
{ SDLK_v, "V", 0 },
{ SDLK_w, "W", 0 },
{ SDLK_x, "X", 0 },
{ SDLK_y, "Y", 0 },
{ SDLK_z, "Z", 0 },
{ SDLK_DELETE, "Delete", "Del" },
if (scancodemap.find(scancode) != scancodemap.end())
return scancodemap.at(scancode).front();
{ SDLK_KP_0, "Numpad 0", "Num0" },
{ SDLK_KP_1, "Numpad 1", "Num1" },
{ SDLK_KP_2, "Numpad 2", "Num2" },
{ SDLK_KP_3, "Numpad 3", "Num3" },
{ SDLK_KP_4, "Numpad 4", "Num4" },
{ SDLK_KP_5, "Numpad 5", "Num5" },
{ SDLK_KP_6, "Numpad 6", "Num6" },
{ SDLK_KP_7, "Numpad 7", "Num7" },
{ SDLK_KP_8, "Numpad 8", "Num8" },
{ SDLK_KP_9, "Numpad 9", "Num9" },
const char* name = SDL_GetScancodeName(scancode);
// Some scancodes have no name, but we must have something to save/load/recognize it, so parse it as SYM_XX
if (strlen(name) == 0)
return CStr("SYM_") + CStr::FromInt(scancode);
return name;
}
{ SDLK_KP_PERIOD, "Numpad .", "NumPoint" },
{ SDLK_KP_DIVIDE, "Numpad /", "NumDivide" },
{ SDLK_KP_MULTIPLY, "Numpad *", "NumMultiply" },
{ SDLK_KP_MINUS, "Numpad -", "NumMinus" },
{ SDLK_KP_PLUS, "Numpad +", "NumPlus" },
{ SDLK_KP_ENTER, "Numpad Enter", "NumEnter" },
{ SDLK_KP_EQUALS, "Numpad =", "NumEquals" }, //?
// Rename some SDL key names (!scancodes) for easier readability.
// NB: this does not intend to be exhaustive, merely cover the usual suspects.
static const std::unordered_map<SDL_Keycode, CStr> keyNames {{
{ SDLK_COMMA, "Comma" },
{ SDLK_SEMICOLON, "Semicolon" },
{ SDLK_COLON, "Colon" },
{ SDLK_PERIOD, "Period" },
{ SDLK_EQUALS, "Equals" },
{ SDLK_PLUS, "Plus" },
{ SDLK_MINUS, "Minus" },
{ SDLK_UP, "Arrow Up", "UpArrow" },
{ SDLK_DOWN, "Arrow Down", "DownArrow" },
{ SDLK_RIGHT, "Arrow Right", "RightArrow" },
{ SDLK_LEFT, "Arrow Left", "LeftArrow" },
{ SDLK_INSERT, "Insert", "Ins" },
{ SDLK_HOME, "Home", 0 },
{ SDLK_END, "End", 0 },
{ SDLK_PAGEUP, "Page Up", "PgUp" },
{ SDLK_PAGEDOWN, "Page Down", "PgDn" },
{ SDLK_QUOTE, "SingleQuote" },
{ SDLK_QUOTEDBL, "DoubleQuote" },
{ SDLK_BACKQUOTE, "BackQuote" },
{ SDLK_F1, "F1", 0 },
{ SDLK_F2, "F2", 0 },
{ SDLK_F3, "F3", 0 },
{ SDLK_F4, "F4", 0 },
{ SDLK_F5, "F5", 0 },
{ SDLK_F6, "F6", 0 },
{ SDLK_F7, "F7", 0 },
{ SDLK_F8, "F8", 0 },
{ SDLK_F9, "F9", 0 },
{ SDLK_F10, "F10", 0 },
{ SDLK_F11, "F11", 0 },
{ SDLK_F12, "F12", 0 },
{ SDLK_F13, "F13", 0 },
{ SDLK_F14, "F14", 0 },
{ SDLK_F15, "F15", 0 },
{ SDLK_LEFTPAREN, { "LeftParen" } },
{ SDLK_NUMLOCKCLEAR, "Num Lock", "NumLock" },
{ SDLK_LEFTBRACKET, { "LeftBracket" } },
{ SDLK_RIGHTBRACKET, { "RightBracket" } },
{ SDLK_BACKSLASH, { "Backslash" } },
{ SDLK_SLASH, { "Slash" } },
{ SDLK_CAPSLOCK, "Caps Lock", "CapsLock" },
{ SDLK_KP_ENTER, "NumEnter" },
{ SDLK_KP_DIVIDE, "NumDivide" },
{ SDLK_KP_MULTIPLY, "NumMultiply" },
{ SDLK_KP_EQUALS, "NumEquals" },
{ SDLK_KP_PERIOD, "NumDecimal" },
{ SDLK_KP_PLUS, "NumPlus" },
{ SDLK_KP_MINUS, "NumMinus" },
{ SDLK_KP_0, "Num0" },
{ SDLK_KP_1, "Num1" },
{ SDLK_KP_2, "Num2" },
{ SDLK_KP_3, "Num3" },
{ SDLK_KP_4, "Num4" },
{ SDLK_KP_5, "Num5" },
{ SDLK_KP_6, "Num6" },
{ SDLK_KP_7, "Num7" },
{ SDLK_KP_8, "Num8" },
{ SDLK_KP_9, "Num9" },
{ SDLK_SCROLLLOCK, "Scroll Lock", "ScrlLock" },
{ SDLK_UP, "" },
{ SDLK_DOWN, "" },
{ SDLK_LEFT, "" },
{ SDLK_RIGHT, "" },
}};
{ SDLK_RSHIFT, "Right Shift", "RightShift" },
{ SDLK_LSHIFT, "Left Shift", "LeftShift" },
{ SDLK_RCTRL, "Right Ctrl", "RightCtrl" },
{ SDLK_LCTRL, "Left Ctrl", "LeftCtrl" },
{ SDLK_RALT, "Right Alt", "RightAlt" },
{ SDLK_LALT, "Left Alt", "LeftAlt" },
{ SDLK_LGUI, "Left Super", "LeftWin" }, /* "Windows" keys */
{ SDLK_RGUI, "Right Super", "RightWin" },
{ SDLK_MODE, "Alt Gr", "AltGr" },
{ SDLK_HELP, "Help", 0 }, // ?
{ SDLK_PRINTSCREEN, "Print Screen", "PrtSc" },
{ SDLK_SYSREQ, "SysRq", 0 },
{ SDLK_STOP, "Break", 0 },
{ SDLK_MENU, "Menu", 0 }, // ?
{ SDLK_POWER, "Power", 0 }, // ?
{ SDLK_UNDO, "Undo", 0 }, // ?
{ MOUSE_LEFT, "Left Mouse Button", "MouseLeft" },
{ MOUSE_RIGHT, "Right Mouse Button", "MouseRight" },
{ MOUSE_MIDDLE, "Middle Mouse Button", "MouseMiddle" },
{ MOUSE_WHEELUP, "Mouse Wheel Up", "WheelUp" },
{ MOUSE_WHEELDOWN, "Mouse Wheel Down", "WheelDown" },
{ MOUSE_X1, "Mouse X1", "MouseX1" },
{ MOUSE_X2, "Mouse X2", "MouseX2" },
{ UNIFIED_SHIFT, "Shift", "AnyShift" },
{ UNIFIED_CTRL, "Ctrl", "AnyCtrl" },
{ UNIFIED_ALT, "Alt", "AnyAlt" },
{ UNIFIED_SUPER, "Super", "AnyWindows" },
{ 0, 0, 0 },
};
void InitKeyNameMap()
CStr FindKeyName(SDL_Scancode scancode)
{
for (const SKeycodeMapping* it = keycodeMapping; it->keycode != 0; ++it)
// Mouse and unified modifiers are harcoded.
if (static_cast<int>(scancode) == UNIFIED_SHIFT)
return "Shift";
else if (static_cast<int>(scancode) == UNIFIED_ALT)
return "Alt";
else if (static_cast<int>(scancode) == UNIFIED_CTRL)
return "Ctrl";
else if (static_cast<int>(scancode) == UNIFIED_SUPER)
return "Super";
else if (static_cast<int>(scancode) == MOUSE_LEFT)
return "MouseLeft";
else if (static_cast<int>(scancode) == MOUSE_RIGHT)
return "MouseRight";
else if (static_cast<int>(scancode) == MOUSE_MIDDLE)
return "MouseMiddle";
else if (static_cast<int>(scancode) == MOUSE_WHEELUP)
return "WheelUp";
else if (static_cast<int>(scancode) == MOUSE_WHEELDOWN)
return "WheelDown";
else if (static_cast<int>(scancode) == MOUSE_X1)
return "WheelLeft";
else if (static_cast<int>(scancode) == MOUSE_X2)
return "WheelRight";
SDL_Keycode code = SDL_GetKeyFromScancode(scancode);
if (keyNames.find(code) != keyNames.end())
return keyNames.at(code);
if (code != SDLK_UNKNOWN)
{
keymap.insert(std::pair<CStr,int>(CStr(it->keyname).LowerCase(), it->keycode));
if(it->altkeyname)
keymap.insert(std::pair<CStr,int>(CStr(it->altkeyname).LowerCase(), it->keycode));
const char* keyName = SDL_GetKeyName(code);
if (strlen(keyName) != 0)
return keyName;
}
// Extra mouse buttons.
for (int i = 1; i < 256; ++i) // There is no mouse 0
{
keymap.insert(std::pair<CStr,int>("mousebutton" + CStr::FromInt(i), MOUSE_BASE + i));
keymap.insert(std::pair<CStr,int>("mousen" + CStr::FromInt(i), MOUSE_BASE + i));
}
}
// Try the scancode name.
const char* name = SDL_GetScancodeName(scancode);
int FindKeyCode(const CStr& keyname)
{
std::map<CStr,int>::iterator it;
it = keymap.find(keyname.LowerCase());
if (it != keymap.end())
return it->second;
return 0;
}
// xxtreme hack: some SDLKeycodes map to chars, and we need to escape [ and \ .
if (keyNames.find(static_cast<SDL_Keycode>(*name)) != keyNames.end())
return keyNames.at(static_cast<SDL_Keycode>(*name));
CStr FindKeyName(int keycode)
{
for (const SKeycodeMapping* it = keycodeMapping; it->keycode != 0; ++it)
if (it->keycode == keycode)
return CStr(it->keyname);
if (strlen(name) != 0)
return name;
return CStr("Unknown");
// Else, show something regardless, so the player knows it's at least recognized.
return CStr("SYM_") + CStr::FromInt(scancode);
}

View file

@ -23,14 +23,16 @@
class CStr8;
extern void InitKeyNameMap();
extern CStr8 FindKeyName(int keycode);
extern int FindKeyCode(const CStr8& keyname);
extern SDL_Scancode FindScancode(const CStr& keyname);
// Map a scancode to a locale-independent scancode name.
extern CStr8 FindScancodeName(SDL_Scancode scancode);
// Map a scancode to a locale-dependent key name (to show the user).
extern CStr8 FindKeyName(SDL_Scancode scancode);
enum {
// Start sequential IDs in the right place
// Pick a code which is greater than any keycodes used by SDL itself
EXTRA_KEYS_BASE = SDL_SCANCODE_TO_KEYCODE(SDL_NUM_SCANCODES),
// Pick a code which is greater than any scancodes used by SDL itself
EXTRA_KEYS_BASE = SDL_NUM_SCANCODES,
// 'Keycodes' for the unified modifier keys
UNIFIED_SHIFT,
UNIFIED_CTRL,

View file

@ -112,6 +112,20 @@ bool JSI_ConfigDB::CreateValue(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate
return true;
}
bool JSI_ConfigDB::CreateValues(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& cfgNsString, const std::string& name, const std::vector<CStr>& values)
{
if (IsProtectedConfigName(name))
return false;
EConfigNamespace cfgNs;
if (!GetConfigNamespace(cfgNsString, cfgNs))
return false;
g_ConfigDB.SetValueList(cfgNs, name, values);
return true;
}
bool JSI_ConfigDB::RemoveValue(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& cfgNsString, const std::string& name)
{
if (IsProtectedConfigName(name))
@ -177,6 +191,7 @@ void JSI_ConfigDB::RegisterScriptFunctions(const ScriptInterface& scriptInterfac
scriptInterface.RegisterFunction<bool, std::wstring, bool, &JSI_ConfigDB::SetChanges>("ConfigDB_SetChanges");
scriptInterface.RegisterFunction<std::string, std::wstring, std::string, &JSI_ConfigDB::GetValue>("ConfigDB_GetValue");
scriptInterface.RegisterFunction<bool, std::wstring, std::string, std::string, &JSI_ConfigDB::CreateValue>("ConfigDB_CreateValue");
scriptInterface.RegisterFunction<bool, std::wstring, std::string, std::vector<CStr>, &JSI_ConfigDB::CreateValues>("ConfigDB_CreateValues");
scriptInterface.RegisterFunction<bool, std::wstring, std::string, &JSI_ConfigDB::RemoveValue>("ConfigDB_RemoveValue");
scriptInterface.RegisterFunction<bool, std::wstring, Path, &JSI_ConfigDB::WriteFile>("ConfigDB_WriteFile");
scriptInterface.RegisterFunction<bool, std::wstring, std::string, std::string, Path, &JSI_ConfigDB::WriteValueToFile>("ConfigDB_WriteValueToFile");

View file

@ -31,6 +31,7 @@ namespace JSI_ConfigDB
bool SetChanges(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& cfgNsString, bool value);
std::string GetValue(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& cfgNsString, const std::string& name);
bool CreateValue(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& cfgNsString, const std::string& name, const std::string& value);
bool CreateValues(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& cfgNsString, const std::string& name, const std::vector<CStr>& values);
bool RemoveValue(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& cfgNsString, const std::string& name);
bool WriteFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& cfgNsString, const Path& path);
bool WriteValueToFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& cfgNsString, const std::string& name, const std::string& value, const Path& path);

View file

@ -0,0 +1,169 @@
/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "JSInterface_Hotkey.h"
#include <unordered_map>
#include <vector>
#include "lib/external_libraries/libsdl.h"
#include "ps/Hotkey.h"
#include "ps/KeyName.h"
#include "scriptinterface/ScriptConversions.h"
/**
* Convert an unordered map to a JS object, mapping keys to values.
* Assumes T to have a c_str() method that returns a const char*
* NB: this is unordered since no particular effort is made to preserve order.
* TODO: this could be moved to ScriptConversions.cpp if the need arises.
*/
template<typename T, typename U>
static void ToJSVal_unordered_map(const ScriptRequest& rq, JS::MutableHandleValue ret, const std::unordered_map<T, U>& val)
{
JS::RootedObject obj(rq.cx, JS_NewPlainObject(rq.cx));
if (!obj)
{
ret.setUndefined();
return;
}
for (const std::pair<T, U>& item : val)
{
JS::RootedValue el(rq.cx);
ScriptInterface::ToJSVal<U>(rq, &el, item.second);
JS_SetProperty(rq.cx, obj, item.first.c_str(), el);
}
ret.setObject(*obj);
}
template<>
void ScriptInterface::ToJSVal<std::unordered_map<std::string, std::vector<std::vector<std::string>>>>(const ScriptRequest& rq, JS::MutableHandleValue ret, const std::unordered_map<std::string, std::vector<std::vector<std::string>>>& val)
{
ToJSVal_unordered_map(rq, ret, val);
}
template<>
void ScriptInterface::ToJSVal<std::unordered_map<std::string, std::string>>(const ScriptRequest& rq, JS::MutableHandleValue ret, const std::unordered_map<std::string, std::string>& val)
{
ToJSVal_unordered_map(rq, ret, val);
}
/**
* @return a (js) object mapping hotkey name (from cfg files) to a list ofscancode names
*/
JS::Value GetHotkeyMap(ScriptInterface::CmptPrivate* pCmptPrivate)
{
ScriptRequest rq(*pCmptPrivate->pScriptInterface);
JS::RootedValue hotkeyMap(rq.cx);
std::unordered_map<std::string, std::vector<std::vector<std::string>>> hotkeys;
for (const std::pair<SDL_Scancode_, KeyMapping>& key : g_HotkeyMap)
for (const SHotkeyMapping& mapping : key.second)
{
std::vector<std::string> keymap;
keymap.push_back(FindScancodeName(static_cast<SDL_Scancode>(key.first)));
for (const SKey& secondary_key : mapping.requires)
keymap.push_back(FindScancodeName(static_cast<SDL_Scancode>(secondary_key.code)));
// All hotkey permutations are present so only push one (arbitrarily).
if (keymap.size() == 1 || keymap[0] < keymap[1])
hotkeys[mapping.name].emplace_back(keymap);
}
pCmptPrivate->pScriptInterface->ToJSVal(rq, &hotkeyMap, hotkeys);
return hotkeyMap;
}
/**
* @return a (js) object mapping scancode names to their locale-dependent name.
*/
JS::Value GetScancodeKeyNames(ScriptInterface::CmptPrivate* pCmptPrivate)
{
ScriptRequest rq(*pCmptPrivate->pScriptInterface);
JS::RootedValue obj(rq.cx);
std::unordered_map<std::string, std::string> map;
// Get the name of all scancodes.
// This is slightly wasteful but should be fine overall, they are dense.
for (int i = 0; i < MOUSE_LAST; ++i)
map[FindScancodeName(static_cast<SDL_Scancode>(i))] = FindKeyName(static_cast<SDL_Scancode>(i));
pCmptPrivate->pScriptInterface->ToJSVal(rq, &obj, map);
return obj;
}
void ReloadHotkeys(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate))
{
UnloadHotkeys();
LoadHotkeys();
}
JS::Value GetConflicts(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue combination)
{
ScriptInterface* scriptInterface = pCmptPrivate->pScriptInterface;
ScriptRequest rq(*scriptInterface);
std::vector<std::string> keys;
if (!scriptInterface->FromJSVal(rq, combination, keys))
{
LOGERROR("Invalid hotkey combination");
return JS::NullValue();
}
if (keys.empty())
return JS::NullValue();
// Pick a random code as a starting point of the hotkeys (they are all equivalent).
SDL_Scancode_ startCode = FindScancode(keys.back());
std::unordered_map<SDL_Scancode_, KeyMapping>::const_iterator it = g_HotkeyMap.find(startCode);
if (it == g_HotkeyMap.end())
return JS::NullValue();
// Create a sorted vector with the remaining keys.
keys.pop_back();
std::set<SKey> codes;
for (const std::string& key : keys)
codes.insert(SKey{ FindScancode(key), false });
std::vector<CStr> conflicts;
// This isn't very efficient, but we shouldn't iterate too many hotkeys
// since we at least have one matching key.
for (const SHotkeyMapping& keymap : it->second)
{
std::set<SKey> match(keymap.requires.begin(), keymap.requires.end());
if (codes == match)
conflicts.emplace_back(keymap.name);
}
if (conflicts.empty())
return JS::NullValue();
JS::RootedValue ret(rq.cx);
scriptInterface->ToJSVal(rq, &ret, conflicts);
return ret;
}
void JSI_Hotkey::RegisterScriptFunctions(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction<JS::Value, &GetHotkeyMap>("GetHotkeyMap");
scriptInterface.RegisterFunction<JS::Value, &GetScancodeKeyNames>("GetScancodeKeyNames");
scriptInterface.RegisterFunction<void, &ReloadHotkeys>("ReloadHotkeys");
scriptInterface.RegisterFunction<JS::Value, JS::HandleValue, &GetConflicts>("GetConflicts");
}

View file

@ -0,0 +1,28 @@
/* Copyright (C) 2020 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* 0 A.D. is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef INCLUDED_JSI_HOTKEY
#define INCLUDED_JSI_HOTKEY
#include "scriptinterface/ScriptInterface.h"
namespace JSI_Hotkey
{
void RegisterScriptFunctions(const ScriptInterface& ScriptInterface);
}
#endif // INCLUDED_JSI_HOTKEY

View file

@ -193,6 +193,7 @@ MESSAGEHANDLER(GuiKeyEvent)
SDL_Event_ ev = { { 0 } };
ev.ev.type = msg->pressed ? SDL_KEYDOWN : SDL_KEYUP;
ev.ev.key.keysym.sym = (SDL_Keycode)(int)msg->sdlkey;
ev.ev.key.keysym.scancode = SDL_GetScancodeFromKey((SDL_Keycode)(int)msg->sdlkey);
in_dispatch_event(&ev);
}