1//============================================================================
2//
3// SSSS tt lll lll
4// SS SS tt ll ll
5// SS tttttt eeee ll ll aaaa
6// SSSS tt ee ee ll ll aa
7// SS tt eeeeee ll ll aaaaa -- "An Atari 2600 VCS Emulator"
8// SS SS tt ee ll ll aa aa
9// SSSS ttt eeeee llll llll aaaaa
10//
11// Copyright (c) 1995-2019 by Bradford W. Mott, Stephen Anthony
12// and the Stella Team
13//
14// See the file "License.txt" for information on usage and redistribution of
15// this file, and for a DISCLAIMER OF ALL WARRANTIES.
16//============================================================================
17
18#include <sstream>
19
20#include "bspf.hxx"
21
22#include "OSystem.hxx"
23#include "GuiObject.hxx"
24#include "PopUpWidget.hxx"
25#include "FrameBuffer.hxx"
26#include "EventHandler.hxx"
27#include "Event.hxx"
28#include "OSystem.hxx"
29#include "EditTextWidget.hxx"
30#include "StringListWidget.hxx"
31#include "Widget.hxx"
32#include "Font.hxx"
33#include "ComboDialog.hxx"
34#include "Variant.hxx"
35#include "EventMappingWidget.hxx"
36
37// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
38EventMappingWidget::EventMappingWidget(GuiObject* boss, const GUI::Font& font,
39 int x, int y, int w, int h,
40 EventMode mode)
41 : Widget(boss, font, x, y, w, h),
42 CommandSender(boss),
43 myFilterPopup(nullptr),
44 myComboDialog(nullptr),
45 myEventMode(mode),
46 myEventGroup(Event::Group::Emulation),
47 myActionSelected(-1),
48 myRemapStatus(false),
49 myLastStick(0),
50 myLastHat(0),
51 myLastAxis(JoyAxis::NONE),
52 myLastDir(JoyDir::NONE),
53 myLastHatDir(JoyHatDir::CENTER),
54 myLastButton(JOY_CTRL_NONE),
55 myFirstTime(true)
56{
57 const int fontHeight = font.getFontHeight(),
58 lineHeight = font.getLineHeight(),
59 buttonWidth = font.getStringWidth("Defaults") + 10,
60 buttonHeight = font.getLineHeight() + 4;
61 const int HBORDER = 8;
62 const int VBORDER = 8;
63 const int ACTION_LINES = 2;
64 int xpos = HBORDER, ypos = VBORDER;
65 const int listWidth = _w - buttonWidth - HBORDER * 2 - 8;
66 int listHeight = _h - (2 + ACTION_LINES) * lineHeight - VBORDER + 2;
67
68 if(mode == EventMode::kEmulationMode)
69 {
70 VariantList items;
71
72 items.clear();
73 VarList::push_back(items, "All", Event::Group::Emulation);
74 VarList::push_back(items, "Miscellaneous", Event::Group::Misc);
75 VarList::push_back(items, "Video & Audio", Event::Group::AudioVideo);
76 VarList::push_back(items, "States", Event::Group::States);
77 VarList::push_back(items, "Console", Event::Group::Console);
78 VarList::push_back(items, "Joystick", Event::Group::Joystick);
79 VarList::push_back(items, "Paddles", Event::Group::Paddles);
80 VarList::push_back(items, "Keyboard", Event::Group::Keyboard);
81 VarList::push_back(items, "Combo", Event::Group::Combo);
82 VarList::push_back(items, "Debug", Event::Group::Debug);
83
84 myFilterPopup = new PopUpWidget(boss, font, xpos, ypos,
85 listWidth - font.getStringWidth("Events ") - 23, lineHeight,
86 items, "Events ", 0, kFilterCmd);
87 myFilterPopup->setTarget(this);
88 addFocusWidget(myFilterPopup);
89 ypos += lineHeight + 8;
90 listHeight -= lineHeight + 8;
91 }
92
93 myActionsList = new StringListWidget(boss, font, xpos, ypos, listWidth, listHeight);
94 myActionsList->setTarget(this);
95 myActionsList->setEditable(false);
96 addFocusWidget(myActionsList);
97
98 // Add remap, erase, cancel and default buttons
99 xpos = _w - HBORDER - buttonWidth;
100 myMapButton = new ButtonWidget(boss, font, xpos, ypos,
101 buttonWidth, buttonHeight,
102 "Map" + ELLIPSIS, kStartMapCmd);
103 myMapButton->setTarget(this);
104 addFocusWidget(myMapButton);
105
106 ypos += lineHeight + 10;
107 myCancelMapButton = new ButtonWidget(boss, font, xpos, ypos,
108 buttonWidth, buttonHeight,
109 "Cancel", kStopMapCmd);
110 myCancelMapButton->setTarget(this);
111 myCancelMapButton->clearFlags(Widget::FLAG_ENABLED);
112 addFocusWidget(myCancelMapButton);
113
114 ypos += lineHeight + 20;
115 myEraseButton = new ButtonWidget(boss, font, xpos, ypos,
116 buttonWidth, buttonHeight,
117 "Erase", kEraseCmd);
118 myEraseButton->setTarget(this);
119 addFocusWidget(myEraseButton);
120
121 ypos += lineHeight + 10;
122 myResetButton = new ButtonWidget(boss, font, xpos, ypos,
123 buttonWidth, buttonHeight,
124 "Reset", kResetCmd);
125 myResetButton->setTarget(this);
126 addFocusWidget(myResetButton);
127
128 if(mode == EventMode::kEmulationMode)
129 {
130 ypos += lineHeight + 20;
131 myComboButton = new ButtonWidget(boss, font, xpos, ypos,
132 buttonWidth, buttonHeight,
133 "Combo" + ELLIPSIS, kComboCmd);
134 myComboButton->setTarget(this);
135 addFocusWidget(myComboButton);
136
137 VariantList combolist = instance().eventHandler().getComboList(mode);
138 myComboDialog = new ComboDialog(boss, font, combolist);
139 }
140 else
141 myComboButton = nullptr;
142
143 // Show message for currently selected event
144 xpos = HBORDER;
145 ypos = myActionsList->getBottom() + 8;
146 StaticTextWidget* t;
147 t = new StaticTextWidget(boss, font, xpos, ypos+2, font.getStringWidth("Action"),
148 fontHeight, "Action", TextAlign::Left);
149
150 myKeyMapping = new EditTextWidget(boss, font, xpos + t->getWidth() + 8, ypos,
151 _w - xpos - t->getWidth() - 8 - HBORDER,
152 lineHeight + font.getFontHeight() * (ACTION_LINES - 1), "");
153 myKeyMapping->setEditable(false, true);
154 myKeyMapping->clearFlags(Widget::FLAG_RETAIN_FOCUS);
155}
156
157// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
158void EventMappingWidget::loadConfig()
159{
160 if(myFirstTime)
161 {
162 if(myFilterPopup)
163 myFilterPopup->setSelectedIndex(0);
164
165 myFirstTime = false;
166 }
167
168 // Make sure remapping is turned off, just in case the user didn't properly
169 // exit last time
170 if(myRemapStatus)
171 stopRemapping();
172
173 updateActions();
174}
175
176// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
177void EventMappingWidget::saveConfig()
178{
179}
180
181// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
182void EventMappingWidget::updateActions()
183{
184 if(myFilterPopup)
185 myEventGroup = Event::Group(myFilterPopup->getSelectedTag().toInt());
186 else
187 myEventGroup = Event::Group::Menu;
188 StringList actions = instance().eventHandler().getActionList(myEventGroup);
189
190 myActionsList->setList(actions);
191 myActionSelected = myActionsList->getSelected();
192 drawKeyMapping();
193 enableButtons(true);
194}
195
196// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
197void EventMappingWidget::setDefaults()
198{
199 instance().eventHandler().setDefaultMapping(Event::NoType, myEventMode);
200 drawKeyMapping();
201}
202
203// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
204void EventMappingWidget::startRemapping()
205{
206 if(myActionSelected < 0 || myRemapStatus)
207 return;
208
209 // Set the flags for the next event that arrives
210 myRemapStatus = true;
211
212 // Reset all previous events for determining correct axis/hat values
213 myLastStick = -1;
214 myLastButton = JOY_CTRL_NONE;
215 myLastAxis = JoyAxis::NONE;
216 myLastDir = JoyDir::NONE;
217 myLastHat = -1;
218 myLastHatDir = JoyHatDir::CENTER;
219
220 // Reset the previously aggregated key mappings
221 myMod = myLastKey = 0;
222
223 // Disable all other widgets while in remap mode, except enable 'Cancel'
224 enableButtons(false);
225
226 // And show a message indicating which key is being remapped
227 ostringstream buf;
228 buf << "Select action for '"
229 << instance().eventHandler().actionAtIndex(myActionSelected, myEventGroup)
230 << "' event";
231 myKeyMapping->setTextColor(kTextColorEm);
232 myKeyMapping->setText(buf.str());
233
234 // Make sure that this widget receives all raw data, before any
235 // pre-processing occurs
236 myActionsList->setFlags(Widget::FLAG_WANTS_RAWDATA);
237}
238
239// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
240void EventMappingWidget::eraseRemapping()
241{
242 if(myActionSelected < 0)
243 return;
244
245 Event::Type event =
246 instance().eventHandler().eventAtIndex(myActionSelected, myEventGroup);
247 instance().eventHandler().eraseMapping(event, myEventMode);
248
249 drawKeyMapping();
250}
251
252// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
253void EventMappingWidget::resetRemapping()
254{
255 if(myActionSelected < 0)
256 return;
257
258 Event::Type event =
259 instance().eventHandler().eventAtIndex(myActionSelected, myEventGroup);
260 instance().eventHandler().setDefaultMapping(event, myEventMode);
261
262 drawKeyMapping();
263}
264
265// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
266void EventMappingWidget::stopRemapping()
267{
268 // Turn off remap mode
269 myRemapStatus = false;
270
271 // Reset all previous events for determining correct axis/hat values
272 myLastStick = -1;
273 myLastButton = JOY_CTRL_NONE;
274 myLastAxis = JoyAxis::NONE;
275 myLastDir = JoyDir::NONE;
276 myLastHat = -1;
277 myLastHatDir = JoyHatDir::CENTER;
278
279 // And re-enable all the widgets
280 enableButtons(true);
281
282 // Make sure the list widget is in a known state
283 drawKeyMapping();
284
285 // Widget is now free to process events normally
286 myActionsList->clearFlags(Widget::FLAG_WANTS_RAWDATA);
287}
288
289// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
290void EventMappingWidget::drawKeyMapping()
291{
292 if(myActionSelected >= 0)
293 {
294 myKeyMapping->setTextColor(kTextColor);
295 myKeyMapping->setText(instance().eventHandler().keyAtIndex(myActionSelected, myEventGroup));
296 }
297}
298
299// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
300void EventMappingWidget::enableButtons(bool state)
301{
302 myActionsList->setEnabled(state);
303 myMapButton->setEnabled(state);
304 myCancelMapButton->setEnabled(!state);
305 myEraseButton->setEnabled(state);
306 myResetButton->setEnabled(state);
307 if(myComboButton)
308 {
309 Event::Type e =
310 instance().eventHandler().eventAtIndex(myActionSelected, myEventGroup);
311
312 myComboButton->setEnabled(state && e >= Event::Combo1 && e <= Event::Combo16);
313 }
314}
315
316// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
317bool EventMappingWidget::handleKeyDown(StellaKey key, StellaMod mod)
318{
319 // Remap keys in remap mode
320 if (myRemapStatus && myActionSelected >= 0)
321 {
322 // Mod keys are only recorded if no other key has been recorded before
323 if (key < KBDK_LCTRL || key > KBDK_RGUI
324 || (!myLastKey || (myLastKey >= KBDK_LCTRL && myLastKey <= KBDK_RGUI)))
325 {
326 myLastKey = key;
327 }
328 myMod |= mod;
329 }
330 return true;
331}
332
333// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
334bool EventMappingWidget::handleKeyUp(StellaKey key, StellaMod mod)
335{
336 // Remap keys in remap mode
337 if (myRemapStatus && myActionSelected >= 0
338 && (mod & (KBDM_CTRL | KBDM_SHIFT | KBDM_ALT | KBDM_GUI)) == 0)
339 {
340 Event::Type event =
341 instance().eventHandler().eventAtIndex(myActionSelected, myEventGroup);
342
343 // if not pressed alone, map left and right modifier keys
344 if(myLastKey < KBDK_LCTRL || myLastKey > KBDK_RGUI)
345 {
346 if(myMod & KBDM_CTRL)
347 myMod |= KBDM_CTRL;
348 if(myMod & KBDM_SHIFT)
349 myMod |= KBDM_SHIFT;
350 if(myMod & KBDM_ALT)
351 myMod |= KBDM_ALT;
352 if(myMod & KBDM_GUI)
353 myMod |= KBDM_GUI;
354 }
355 if (instance().eventHandler().addKeyMapping(event, myEventMode, StellaKey(myLastKey), StellaMod(myMod)))
356 stopRemapping();
357 }
358 return true;
359}
360
361// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
362void EventMappingWidget::handleJoyDown(int stick, int button, bool longPress)
363{
364 // Remap joystick buttons in remap mode
365 if(myRemapStatus && myActionSelected >= 0)
366 {
367 myLastStick = stick;
368 myLastButton = button;
369 }
370}
371
372// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
373void EventMappingWidget::handleJoyUp(int stick, int button)
374{
375 // Remap joystick buttons in remap mode
376 if (myRemapStatus && myActionSelected >= 0)
377 {
378 if (myLastStick == stick && myLastButton == button)
379 {
380 EventHandler& eh = instance().eventHandler();
381 Event::Type event = eh.eventAtIndex(myActionSelected, myEventGroup);
382
383 // map either button/hat, solo button or button/axis combinations
384 if(myLastHat != -1)
385 {
386 if(eh.addJoyHatMapping(event, myEventMode, stick, button, myLastHat, myLastHatDir))
387 stopRemapping();
388 }
389 else
390 if (eh.addJoyMapping(event, myEventMode, stick, button, myLastAxis, myLastDir))
391 stopRemapping();
392 }
393 }
394}
395
396// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
397void EventMappingWidget::handleJoyAxis(int stick, JoyAxis axis, JoyDir adir, int button)
398{
399 // Remap joystick axes in remap mode
400 // There are two phases to detection:
401 // First, detect an axis 'on' event
402 // Then, detect the same axis 'off' event
403 if(myRemapStatus && myActionSelected >= 0)
404 {
405 // Detect the first axis event that represents 'on'
406 if((myLastStick == -1 || myLastStick == stick) && myLastAxis == JoyAxis::NONE && adir != JoyDir::NONE)
407 {
408 myLastStick = stick;
409 myLastAxis = axis;
410 myLastDir = adir;
411 }
412 // Detect the first axis event that matches a previously set
413 // stick and axis, but turns the axis 'off'
414 else if(myLastStick == stick && axis == myLastAxis && adir == JoyDir::NONE)
415 {
416 EventHandler& eh = instance().eventHandler();
417 Event::Type event = eh.eventAtIndex(myActionSelected, myEventGroup);
418
419 if (eh.addJoyMapping(event, myEventMode, stick, myLastButton, axis, myLastDir))
420 stopRemapping();
421 }
422 }
423}
424
425// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
426bool EventMappingWidget::handleJoyHat(int stick, int hat, JoyHatDir hdir, int button)
427{
428 // Remap joystick hats in remap mode
429 // There are two phases to detection:
430 // First, detect a hat direction event
431 // Then, detect the same hat 'center' event
432 if(myRemapStatus && myActionSelected >= 0)
433 {
434 // Detect the first hat event that represents a valid direction
435 if((myLastStick == -1 || myLastStick == stick) && myLastHat == -1 && hdir != JoyHatDir::CENTER)
436 {
437 myLastStick = stick;
438 myLastHat = hat;
439 myLastHatDir = hdir;
440
441 return true;
442 }
443 // Detect the first hat event that matches a previously set
444 // stick and hat, but centers the hat
445 else if(myLastStick == stick && hat == myLastHat && hdir == JoyHatDir::CENTER)
446 {
447 EventHandler& eh = instance().eventHandler();
448 Event::Type event = eh.eventAtIndex(myActionSelected, myEventGroup);
449
450 if (eh.addJoyHatMapping(event, myEventMode, stick, myLastButton, hat, myLastHatDir))
451 {
452 stopRemapping();
453 return true;
454 }
455 }
456 }
457
458 return false;
459}
460
461// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
462void EventMappingWidget::handleCommand(CommandSender* sender, int cmd,
463 int data, int id)
464{
465 switch(cmd)
466 {
467 case kFilterCmd:
468 updateActions();
469 break;
470
471 case ListWidget::kSelectionChangedCmd:
472 if(myActionsList->getSelected() >= 0)
473 {
474 myActionSelected = myActionsList->getSelected();
475 drawKeyMapping();
476 enableButtons(true);
477 }
478 break;
479
480 case ListWidget::kDoubleClickedCmd:
481 if(myActionsList->getSelected() >= 0)
482 {
483 myActionSelected = myActionsList->getSelected();
484 startRemapping();
485 }
486 break;
487
488 case kStartMapCmd:
489 startRemapping();
490 break;
491
492 case kStopMapCmd:
493 stopRemapping();
494 break;
495
496 case kEraseCmd:
497 eraseRemapping();
498 break;
499
500 case kResetCmd:
501 resetRemapping();
502 break;
503
504 case kComboCmd:
505 if(myComboDialog)
506 myComboDialog->show(
507 instance().eventHandler().eventAtIndex(myActionSelected, myEventGroup),
508 instance().eventHandler().actionAtIndex(myActionSelected, myEventGroup));
509 break;
510 }
511}
512