1 /*
2 This file is part of Warzone 2100.
3 Copyright (C) 1999-2004 Eidos Interactive
4 Copyright (C) 2005-2020 Warzone 2100 Project
5
6 Warzone 2100 is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10
11 Warzone 2100 is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Warzone 2100; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 */
20 /*
21 * keyedit.c
22 * keymap editor
23 * alexl.
24 */
25
26 // ////////////////////////////////////////////////////////////////////////////
27 // includes
28 #include <string.h>
29 #include <physfs.h>
30
31 #include "lib/framework/frame.h"
32 #include "lib/framework/frameresource.h"
33 #include "lib/ivis_opengl/bitimage.h"
34 #include "lib/ivis_opengl/pieblitfunc.h"
35 #include "lib/sound/audio.h"
36 #include "lib/sound/audio_id.h"
37 #include "lib/widget/button.h"
38 #include "lib/widget/scrollablelist.h"
39
40 #include "frend.h"
41 #include "frontend.h"
42 #include "hci.h"
43 #include "init.h"
44 #include "intdisplay.h"
45 #include "keybind.h"
46 #include "keyedit.h"
47 #include "keymap.h"
48 #include "loadsave.h"
49 #include "main.h"
50 #include "multiint.h"
51 #include "multiplay.h"
52 #include "ingameop.h"
53
54 // ////////////////////////////////////////////////////////////////////////////
55 // defines
56
57 #define KM_START 10204
58 #define KM_END 10399
59
60 #define KM_W FRONTEND_BOTFORMW
61 #define KM_H 440
62 #define KM_X FRONTEND_BOTFORMX
63 #define KM_Y 20
64 #define KM_SX FRONTEND_SIDEX
65
66
67 #define KM_ENTRYW (FRONTEND_BOTFORMW - 80)
68 #define KM_ENTRYH (16)
69
70 class KeyMapForm : public IntFormAnimated
71 {
72 protected:
KeyMapForm()73 KeyMapForm(): IntFormAnimated(false) {}
74 void initialize(bool isInGame);
75
76 public:
make(bool isInGame)77 static std::shared_ptr<KeyMapForm> make(bool isInGame)
78 {
79 class make_shared_enabler: public KeyMapForm {};
80 auto widget = std::make_shared<make_shared_enabler>();
81 widget->initialize(isInGame);
82 return widget;
83 }
84
85 void checkPushedKeyCombo();
86 bool pushedKeyCombo(KEY_CODE subkey);
87
88 private:
89 std::shared_ptr<ScrollableListWidget> keyMapList;
90 void unhighlightSelected();
91 void addButton(int buttonId, int y, const char *text);
92 };
93
94 struct DisplayKeyMapCache {
95 WzText wzNameText;
96 WzText wzBindingText;
97 };
98
99 struct DisplayKeyMapData {
DisplayKeyMapDataDisplayKeyMapData100 explicit DisplayKeyMapData(KEY_MAPPING *psMapping)
101 : psMapping(psMapping)
102 { }
103
104 KEY_MAPPING *psMapping;
105 DisplayKeyMapCache cache;
106 };
107
108 // ////////////////////////////////////////////////////////////////////////////
109 // variables
110
111 static KEY_MAPPING *selectedKeyMap;
112 static bool maxKeyMapNameWidthDirty = true;
113
114 // ////////////////////////////////////////////////////////////////////////////
115 // funcs
116 // ////////////////////////////////////////////////////////////////////////////
scanKeyBoardForBinding()117 static KEY_CODE scanKeyBoardForBinding()
118 {
119 UDWORD i;
120 for (i = 0; i < KEY_MAXSCAN; i++)
121 {
122 if (keyPressed((KEY_CODE)i))
123 {
124 if (i != KEY_RALT // exceptions
125 && i != KEY_LALT
126 && i != KEY_RCTRL
127 && i != KEY_LCTRL
128 && i != KEY_RSHIFT
129 && i != KEY_LSHIFT
130 && i != KEY_LMETA
131 && i != KEY_RMETA
132 )
133 {
134 return (KEY_CODE)i; // top row key pressed
135 }
136 }
137 }
138 return (KEY_CODE)0;
139 }
140
runInGameKeyMapEditor(unsigned id)141 bool runInGameKeyMapEditor(unsigned id)
142 {
143 if (id == KM_RETURN || id == KM_GO_BACK) // return
144 {
145 saveKeyMap();
146 widgDelete(psWScreen, KM_FORM);
147 inputLoseFocus();
148 bAllowOtherKeyPresses = true;
149 if (id == KM_GO_BACK)
150 {
151 intReopenMenuWithoutUnPausing();
152 return false;
153 }
154 return true;
155 }
156 if (id == KM_DEFAULT)
157 {
158 // reinitialise key mappings
159 keyInitMappings(true);
160 widgDelete(psWScreen, KM_FORM); // readd the widgets
161 startInGameKeyMapEditor(false);
162 maxKeyMapNameWidthDirty = true;
163 }
164
165 if (auto kmForm = (KeyMapForm *)widgGetFromID(psWScreen, KM_FORM))
166 {
167 kmForm->checkPushedKeyCombo();
168 }
169 return false;
170 }
171
172 // ////////////////////////////////////////////////////////////////////////////
runKeyMapEditor()173 bool runKeyMapEditor()
174 {
175 WidgetTriggers const &triggers = widgRunScreen(psWScreen);
176 unsigned id = triggers.empty() ? 0 : triggers.front().widget->id; // Just use first click here, since the next click could be on another menu.
177
178 if (id == KM_RETURN) // return
179 {
180 saveKeyMap();
181 changeTitleMode(OPTIONS);
182 }
183 if (id == KM_DEFAULT)
184 {
185 // reinitialise key mappings
186 keyInitMappings(true);
187 widgDelete(psWScreen, FRONTEND_BACKDROP); // readd the widgets
188 startKeyMapEditor(false);
189 }
190
191 if (auto kmForm = (KeyMapForm *)widgGetFromID(psWScreen, KM_FORM))
192 {
193 kmForm->checkPushedKeyCombo();
194 }
195
196 widgDisplayScreen(psWScreen); // show the widgets currently running
197
198 if (CancelPressed())
199 {
200 changeTitleMode(OPTIONS);
201 }
202
203 return true;
204 }
205
206 // ////////////////////////////////////////////////////////////////////////////
207 // returns key to press given a mapping.
keyMapToString(char * pStr,KEY_MAPPING * psMapping)208 static bool keyMapToString(char *pStr, KEY_MAPPING *psMapping)
209 {
210 bool onlySub = true;
211 char asciiSub[20], asciiMeta[20];
212
213 if (psMapping->metaKeyCode != KEY_IGNORE)
214 {
215 keyScanToString(psMapping->metaKeyCode, (char *)&asciiMeta, 20);
216 onlySub = false;
217 }
218 keyScanToString(psMapping->subKeyCode, (char *)&asciiSub, 20);
219
220 if (onlySub)
221 {
222 sprintf(pStr, "%s", asciiSub);
223 }
224 else
225 {
226 sprintf(pStr, "%s %s", asciiMeta, asciiSub);
227 }
228 return true;
229 }
230
getVisibleMappings()231 std::vector<KEY_MAPPING *> getVisibleMappings()
232 {
233 std::vector<KEY_MAPPING *> mappings;
234 for (KEY_MAPPING &mapping : keyMappings)
235 {
236 if (mapping.status != KEYMAP__DEBUG && mapping.status != KEYMAP___HIDE)
237 {
238 mappings.push_back(&mapping);
239 }
240 }
241
242 return mappings;
243 }
244
getMaxKeyMapNameWidth()245 static uint16_t getMaxKeyMapNameWidth()
246 {
247 static uint16_t max = 0;
248
249 if (maxKeyMapNameWidthDirty) {
250 max = 0;
251 WzText displayText;
252 char sKey[MAX_STR_LENGTH];
253
254 for (auto mapping: getVisibleMappings()) {
255 keyMapToString(sKey, mapping);
256 displayText.setText(sKey, font_regular);
257 max = MAX(max, displayText.width());
258 }
259
260 maxKeyMapNameWidthDirty = false;
261 }
262
263 return max;
264 }
265
266 // ////////////////////////////////////////////////////////////////////////////
267 // display a keymap on the interface.
displayKeyMap(WIDGET * psWidget,UDWORD xOffset,UDWORD yOffset)268 static void displayKeyMap(WIDGET *psWidget, UDWORD xOffset, UDWORD yOffset)
269 {
270 // Any widget using displayKeyMap must have its pUserData initialized to a (DisplayKeyMapData*)
271 assert(psWidget->pUserData != nullptr);
272 DisplayKeyMapData& data = *static_cast<DisplayKeyMapData *>(psWidget->pUserData);
273
274 int x = xOffset + psWidget->x();
275 int y = yOffset + psWidget->y();
276 int w = psWidget->width();
277 int h = psWidget->height();
278 KEY_MAPPING *psMapping = data.psMapping;
279 char sKey[MAX_STR_LENGTH];
280
281 if (psMapping == selectedKeyMap)
282 {
283 pie_BoxFill(x, y, x + w, y + h, WZCOL_KEYMAP_ACTIVE);
284 }
285 else if (psMapping->status == KEYMAP_ALWAYS || psMapping->status == KEYMAP_ALWAYS_PROCESS)
286 {
287 // when user can't edit something...
288 pie_BoxFill(x, y , x + w, y + h, WZCOL_KEYMAP_FIXED);
289 }
290 else
291 {
292 drawBlueBoxInset(x, y, w, h);
293 }
294
295 // draw name
296 data.cache.wzNameText.setText(_(psMapping->name.c_str()), font_regular);
297 data.cache.wzNameText.render(x + 2, y + (psWidget->height() / 2) + 3, WZCOL_FORM_TEXT);
298
299 // draw binding
300 keyMapToString(sKey, psMapping);
301 // Check to see if key is on the numpad, if so tell user and change color
302 PIELIGHT bindingTextColor = WZCOL_FORM_TEXT;
303 if (psMapping->subKeyCode >= KEY_KP_0 && psMapping->subKeyCode <= KEY_KPENTER)
304 {
305 bindingTextColor = WZCOL_YELLOW;
306 }
307 data.cache.wzBindingText.setText(sKey, font_regular);
308 data.cache.wzBindingText.render(x + psWidget->width() - getMaxKeyMapNameWidth() - 2, y + (psWidget->height() / 2) + 3, bindingTextColor);
309 }
310
311 // ////////////////////////////////////////////////////////////////////////////
keyMapEditor(bool first,WIDGET * parent,bool isInGame)312 static bool keyMapEditor(bool first, WIDGET *parent, bool isInGame)
313 {
314 if (first)
315 {
316 loadKeyMap(); // get the current mappings.
317 }
318
319 parent->attach(KeyMapForm::make(isInGame));
320
321 /* Stop when the right number or when alphabetically last - not sure...! */
322 /* Go home... */
323 return true;
324 }
325
startInGameKeyMapEditor(bool first)326 bool startInGameKeyMapEditor(bool first)
327 {
328 bAllowOtherKeyPresses = false;
329 return keyMapEditor(first, psWScreen->psForm.get(), true);
330 }
331
startKeyMapEditor(bool first)332 bool startKeyMapEditor(bool first)
333 {
334 addBackdrop();
335 addSideText(FRONTEND_SIDETEXT, KM_SX, KM_Y, _("KEY MAPPING"));
336 WIDGET *parent = widgGetFromID(psWScreen, FRONTEND_BACKDROP);
337 return keyMapEditor(first, parent, false);
338 }
339
340 // ////////////////////////////////////////////////////////////////////////////
341 // save current keymaps to registry
saveKeyMap()342 bool saveKeyMap()
343 {
344 WzConfig ini(KeyMapPath, WzConfig::ReadAndWrite);
345 if (!ini.status() || !ini.isWritable())
346 {
347 // NOTE: Changed to LOG_FATAL, since we want to inform user via pop-up (windows only)
348 debug(LOG_FATAL, "Could not open %s", ini.fileName().toUtf8().c_str());
349 return false;
350 }
351
352 ini.setValue("version", 1);
353
354 ini.beginArray("mappings");
355 for (auto const &mapping : keyMappings)
356 {
357 ini.setValue("name", mapping.name.c_str());
358 ini.setValue("status", mapping.status);
359 ini.setValue("meta", mapping.metaKeyCode);
360 ini.setValue("sub", mapping.subKeyCode);
361 ini.setValue("action", mapping.action);
362 if (auto function = keymapEntryByFunction(mapping.function))
363 {
364 ini.setValue("function", function->name);
365 }
366
367 ini.nextArrayItem();
368 }
369 ini.endArray();
370
371 debug(LOG_WZ, "Keymap written ok to %s.", KeyMapPath);
372 return true; // saved ok.
373 }
374
375 // ////////////////////////////////////////////////////////////////////////////
376 // load keymaps from registry.
loadKeyMap()377 bool loadKeyMap()
378 {
379 // throw away any keymaps!!
380 keyMappings.clear();
381
382 WzConfig ini(KeyMapPath, WzConfig::ReadOnly);
383 if (!ini.status())
384 {
385 debug(LOG_WZ, "%s not found", KeyMapPath);
386 return false;
387 }
388
389 for (ini.beginArray("mappings"); ini.remainingArrayItems(); ini.nextArrayItem())
390 {
391 auto name = ini.value("name", "").toWzString();
392 auto status = (KEY_STATUS)ini.value("status", 0).toInt();
393 auto meta = (KEY_CODE)ini.value("meta", 0).toInt();
394 auto sub = (KEY_CODE)ini.value("sub", 0).toInt();
395 auto action = (KEY_ACTION)ini.value("action", 0).toInt();
396 auto functionName = ini.value("function", "").toWzString();
397 auto function = keymapEntryByName(functionName.toUtf8());
398 if (function == nullptr)
399 {
400 debug(LOG_WARNING, "Skipping unknown keymap function \"%s\".", functionName.toUtf8().c_str());
401 continue;
402 }
403
404 // add mapping
405 keyAddMapping(status, meta, sub, action, function->function, name.toUtf8().c_str());
406 }
407 ini.endArray();
408 return true;
409 }
410
initialize(bool isInGame)411 void KeyMapForm::initialize(bool isInGame)
412 {
413 id = KM_FORM;
414
415 attach(keyMapList = ScrollableListWidget::make());
416 if (!isInGame)
417 {
418 setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
419 psWidget->setGeometry(KM_X, KM_Y, KM_W, KM_H);
420 }));
421 keyMapList->setGeometry(52, 10, KM_ENTRYW, 26 * KM_ENTRYH);
422
423 addMultiBut(*this, KM_RETURN, // return button.
424 8, 5,
425 iV_GetImageWidth(FrontImages, IMAGE_RETURN),
426 iV_GetImageHeight(FrontImages, IMAGE_RETURN),
427 _("Return To Previous Screen"), IMAGE_RETURN, IMAGE_RETURN_HI, IMAGE_RETURN_HI);
428
429 addMultiBut(*this, KM_DEFAULT,
430 11, 45,
431 iV_GetImageWidth(FrontImages, IMAGE_KEYMAP_DEFAULT),
432 iV_GetImageHeight(FrontImages, IMAGE_KEYMAP_DEFAULT),
433 _("Select Default"),
434 IMAGE_KEYMAP_DEFAULT, IMAGE_KEYMAP_DEFAULT_HI, IMAGE_KEYMAP_DEFAULT_HI); // default.
435 }
436 else
437 {
438 // Text versions for in-game where image resources are not available
439 setCalcLayout(LAMBDA_CALCLAYOUT_SIMPLE({
440 psWidget->setGeometry(((300-(KM_W/2))+D_W), ((240-(KM_H/2))+D_H), KM_W, KM_H + 10);
441 }));
442 keyMapList->setGeometry(52, 10, KM_ENTRYW, 24 * KM_ENTRYH);
443
444 addButton(KM_GO_BACK, KM_H - 40, _("Go Back"));
445
446 addButton(KM_RETURN, KM_H - 24, _("Resume Game"));
447
448 if (!(bMultiPlayer && NetPlay.bComms != 0)) // no editing in true multiplayer
449 {
450 addButton(KM_DEFAULT, KM_H - 8, _("Select Default"));
451 }
452 }
453
454 //Put the buttons on it
455 auto mappings = getVisibleMappings();
456
457 std::sort(mappings.begin(), mappings.end(), [](KEY_MAPPING *a, KEY_MAPPING *b) {
458 return a->name < b->name;
459 });
460 /* Add our first mapping to the form */
461 /* Now add the others... */
462 for (std::vector<KEY_MAPPING *>::const_iterator i = mappings.begin(); i != mappings.end(); ++i)
463 {
464 W_BUTINIT emptyInit;
465 auto button = std::make_shared<W_BUTTON>(&emptyInit);
466 button->setGeometry(0, 0, KM_ENTRYW, KM_ENTRYH);
467 button->id = KM_START + (i - mappings.begin());
468 button->displayFunction = displayKeyMap;
469 button->pUserData = new DisplayKeyMapData(*i);
470 button->setOnDelete([](WIDGET *psWidget) {
471 assert(psWidget->pUserData != nullptr);
472 delete static_cast<DisplayKeyMapData *>(psWidget->pUserData);
473 psWidget->pUserData = nullptr;
474 });
475 button->addOnClickHandler([=](W_BUTTON& clickedButton) {
476 auto clickedMapping = static_cast<DisplayKeyMapData *>(clickedButton.pUserData)->psMapping;
477 if (!clickedMapping || clickedMapping->status != KEYMAP_ASSIGNABLE)
478 {
479 audio_PlayTrack(ID_SOUND_BUILD_FAIL);
480 } else if (selectedKeyMap == clickedMapping) {
481 unhighlightSelected();
482 } else {
483 keyMapList->disableScroll();
484 selectedKeyMap = clickedMapping;
485 }
486 });
487 keyMapList->addItem(button);
488 }
489 }
490
addButton(int buttonId,int y,const char * text)491 void KeyMapForm::addButton(int buttonId, int y, const char *text)
492 {
493 W_BUTINIT sButInit;
494
495 sButInit.formID = KM_FORM;
496 sButInit.style = WBUT_PLAIN | WBUT_TXTCENTRE;
497 sButInit.width = KM_W;
498 sButInit.FontID = font_regular;
499 sButInit.x = 0;
500 sButInit.height = 10;
501 sButInit.pDisplay = displayTextOption;
502 sButInit.initPUserDataFunc = []() -> void * { return new DisplayTextOptionCache(); };
503 sButInit.onDelete = [](WIDGET *psWidget) {
504 assert(psWidget->pUserData != nullptr);
505 delete static_cast<DisplayTextOptionCache *>(psWidget->pUserData);
506 psWidget->pUserData = nullptr;
507 };
508
509 sButInit.id = buttonId;
510 sButInit.y = y;
511 sButInit.pText = text;
512
513 attach(std::make_shared<W_BUTTON>(&sButInit));
514 }
515
checkPushedKeyCombo()516 void KeyMapForm::checkPushedKeyCombo()
517 {
518 if (selectedKeyMap)
519 {
520 KEY_CODE kc = scanKeyBoardForBinding();
521 if (kc)
522 {
523 pushedKeyCombo(kc);
524 }
525 }
526 }
527
pushedKeyCombo(KEY_CODE subkey)528 bool KeyMapForm::pushedKeyCombo(KEY_CODE subkey)
529 {
530 KEY_CODE metakey = KEY_IGNORE;
531 KEY_MAPPING *pExist;
532 KEY_MAPPING *psMapping;
533 KEY_CODE alt;
534
535 // check for
536 // alt
537 alt = (KEY_CODE)0;
538 if (keyDown(KEY_RALT) || keyDown(KEY_LALT))
539 {
540 metakey = KEY_LALT;
541 alt = KEY_RALT;
542 }
543 // ctrl
544 else if (keyDown(KEY_RCTRL) || keyDown(KEY_LCTRL))
545 {
546 metakey = KEY_LCTRL;
547 alt = KEY_RCTRL;
548 }
549 // shift
550 else if (keyDown(KEY_RSHIFT) || keyDown(KEY_LSHIFT))
551 {
552 metakey = KEY_LSHIFT;
553 alt = KEY_RSHIFT;
554 }
555 // meta (cmd)
556 else if (keyDown(KEY_RMETA) || keyDown(KEY_LMETA))
557 {
558 metakey = KEY_LMETA;
559 alt = KEY_RMETA;
560 }
561
562 // check if bound to a fixed combo.
563 pExist = keyFindMapping(metakey, subkey);
564 if (pExist && (pExist->status == KEYMAP_ALWAYS || pExist->status == KEYMAP_ALWAYS_PROCESS))
565 {
566 unhighlightSelected();
567 return false;
568 }
569
570 /* Clear down mappings using these keys... But only if it isn't unassigned */
571 keyReAssignMapping(metakey, subkey, KEY_IGNORE, (KEY_CODE)KEY_MAXSCAN);
572
573 /* Try and see if its there already - damn well should be! */
574 psMapping = keyGetMappingFromFunction(selectedKeyMap->function);
575
576 /* Cough if it's not there */
577 ASSERT_OR_RETURN(false, psMapping != nullptr, "Trying to patch a non-existent function mapping - whoop whoop!!!");
578
579 /* Now alter it to the new values */
580 psMapping->metaKeyCode = metakey;
581 psMapping->subKeyCode = subkey;
582 // was "=="
583 psMapping->status = KEYMAP_ASSIGNABLE; //must be
584 if (alt)
585 {
586 psMapping->altMetaKeyCode = alt;
587 }
588 unhighlightSelected();
589 maxKeyMapNameWidthDirty = true;
590 return true;
591 }
592
unhighlightSelected()593 void KeyMapForm::unhighlightSelected()
594 {
595 keyMapList->enableScroll();
596 selectedKeyMap = nullptr;
597 }
598