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