1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2001, Sven2
6 * Copyright (c) 2017-2020, The LegacyClonk Team and contributors
7 *
8 * Distributed under the terms of the ISC license; see accompanying file
9 * "COPYING" for details.
10 *
11 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
12 * See accompanying file "TRADEMARK" for details.
13 *
14 * To redistribute this file separately, substitute the full license texts
15 * for the above references.
16 */
17
18// generic user interface
19// context menu
20
21#include "C4GuiResource.h"
22#include <C4Include.h>
23#include <C4Gui.h>
24#include <C4FacetEx.h>
25#include <C4Wrappers.h>
26#include <C4MouseControl.h>
27
28#include <StdWindow.h>
29
30namespace C4GUI
31{
32
33int32_t ContextMenu::iGlobalMenuIndex = 0;
34
35// ContextMenu::Entry
36
37void ContextMenu::Entry::DrawElement(C4FacetEx &cgo)
38{
39 // icon
40 if (icoIcon > Ico_None)
41 {
42 // get icon counts
43 int32_t iXMax, iYMax;
44 GetRes()->fctIcons.GetPhaseNum(rX&: iXMax, rY&: iYMax);
45 if (!iXMax)
46 iXMax = 6;
47 // load icon
48 const C4Facet &rfctIcon = GetRes()->fctIcons.GetPhase(iPhaseX: icoIcon % iXMax, iPhaseY: icoIcon / iXMax);
49 rfctIcon.DrawX(sfcTarget: cgo.Surface, iX: rcBounds.x + cgo.TargetX, iY: rcBounds.y + cgo.TargetY, iWdt: rcBounds.Hgt, iHgt: rcBounds.Hgt);
50 }
51 // print out label
52 if (!!sText)
53 lpDDraw->TextOut(szText: sText.getData(), rFont&: GetRes()->TextFont, fZoom: 1.0f, sfcDest: cgo.Surface, iTx: cgo.TargetX + rcBounds.x + GetIconIndent(), iTy: rcBounds.y + cgo.TargetY, C4GUI_ContextFontClr, byForm: ALeft);
54 // submenu arrow
55 if (pSubmenuHandler)
56 {
57 C4Facet &rSubFct = GetRes()->fctSubmenu;
58 rSubFct.Draw(sfcTarget: cgo.Surface, iX: cgo.TargetX + rcBounds.x + rcBounds.Wdt - rSubFct.Wdt, iY: cgo.TargetY + rcBounds.y + (rcBounds.Hgt - rSubFct.Hgt) / 2);
59 }
60}
61
62ContextMenu::Entry::Entry(const char *szText, Icons icoIcon, MenuHandler *pMenuHandler, ContextHandler *pSubmenuHandler)
63 : Element(), cHotkey(0), icoIcon(icoIcon), pMenuHandler(pMenuHandler), pSubmenuHandler(pSubmenuHandler)
64{
65 // set text with hotkey
66 if (szText)
67 {
68 sText.Copy(pnData: szText);
69 ExpandHotkeyMarkup(sText, rcHotkey&: cHotkey);
70 // adjust size
71 GetRes()->TextFont.GetTextExtent(szText: sText.getData(), rsx&: rcBounds.Wdt, rsy&: rcBounds.Hgt, fCheckMarkup: true);
72 }
73 else
74 {
75 rcBounds.Wdt = 40;
76 rcBounds.Hgt = GetRes()->TextFont.GetLineHeight();
77 }
78 // regard icon
79 rcBounds.Wdt += GetIconIndent();
80 // submenu arrow
81 if (pSubmenuHandler) rcBounds.Wdt += GetRes()->fctSubmenu.Wdt + 2;
82}
83
84// ContextMenu
85
86ContextMenu::ContextMenu() : Window(), pTarget(nullptr), pSelectedItem(nullptr), pSubmenu(nullptr)
87{
88 iMenuIndex = ++iGlobalMenuIndex;
89 // set min size
90 rcBounds.Wdt = 40; rcBounds.Hgt = 7;
91 // key bindings
92 C4CustomKey::CodeList Keys;
93 Keys.push_back(x: C4KeyCodeEx(K_UP));
94 if (Config.Controls.GamepadGuiControl)
95 {
96 Keys.push_back(x: C4KeyCodeEx(KEY_Gamepad(idGamepad: 0, idButton: KEY_JOY_Up)));
97 }
98 pKeySelUp = new C4KeyBinding(Keys, "GUIContextSelUp", KEYSCOPE_Gui,
99 new C4KeyCB<ContextMenu>(*this, &ContextMenu::KeySelUp), C4CustomKey::PRIO_Context);
100
101 Keys.clear();
102 Keys.push_back(x: C4KeyCodeEx(K_DOWN));
103 if (Config.Controls.GamepadGuiControl)
104 {
105 Keys.push_back(x: C4KeyCodeEx(KEY_Gamepad(idGamepad: 0, idButton: KEY_JOY_Down)));
106 }
107 pKeySelDown = new C4KeyBinding(Keys, "GUIContextSelDown", KEYSCOPE_Gui,
108 new C4KeyCB<ContextMenu>(*this, &ContextMenu::KeySelDown), C4CustomKey::PRIO_Context);
109
110 Keys.clear();
111 Keys.push_back(x: C4KeyCodeEx(K_RIGHT));
112 if (Config.Controls.GamepadGuiControl)
113 {
114 Keys.push_back(x: C4KeyCodeEx(KEY_Gamepad(idGamepad: 0, idButton: KEY_JOY_Right)));
115 }
116 pKeySubmenu = new C4KeyBinding(Keys, "GUIContextSubmenu", KEYSCOPE_Gui,
117 new C4KeyCB<ContextMenu>(*this, &ContextMenu::KeySubmenu), C4CustomKey::PRIO_Context);
118
119 Keys.clear();
120 Keys.push_back(x: C4KeyCodeEx(K_LEFT));
121 if (Config.Controls.GamepadGuiControl)
122 {
123 Keys.push_back(x: C4KeyCodeEx(KEY_Gamepad(idGamepad: 0, idButton: KEY_JOY_Left)));
124 }
125 pKeyBack = new C4KeyBinding(Keys, "GUIContextBack", KEYSCOPE_Gui,
126 new C4KeyCB<ContextMenu>(*this, &ContextMenu::KeyBack), C4CustomKey::PRIO_Context);
127
128 Keys.clear();
129 Keys.push_back(x: C4KeyCodeEx(K_ESCAPE));
130 if (Config.Controls.GamepadGuiControl)
131 {
132 Keys.push_back(x: C4KeyCodeEx(KEY_Gamepad(idGamepad: 0, idButton: KEY_JOY_AnyHighButton)));
133 }
134 pKeyAbort = new C4KeyBinding(Keys, "GUIContextAbort", KEYSCOPE_Gui,
135 new C4KeyCB<ContextMenu>(*this, &ContextMenu::KeyAbort), C4CustomKey::PRIO_Context);
136
137 Keys.clear();
138 Keys.push_back(x: C4KeyCodeEx(K_RETURN));
139 if (Config.Controls.GamepadGuiControl)
140 {
141 Keys.push_back(x: C4KeyCodeEx(KEY_Gamepad(idGamepad: 0, idButton: KEY_JOY_AnyLowButton)));
142 }
143 pKeyConfirm = new C4KeyBinding(Keys, "GUIContextConfirm", KEYSCOPE_Gui,
144 new C4KeyCB<ContextMenu>(*this, &ContextMenu::KeyConfirm), C4CustomKey::PRIO_Context);
145
146 pKeyHotkey = new C4KeyBinding(C4KeyCodeEx(KEY_Any), "GUIContextHotkey", KEYSCOPE_Gui,
147 new C4KeyCBPassKey<ContextMenu>(*this, &ContextMenu::KeyHotkey), C4CustomKey::PRIO_Context);
148}
149
150ContextMenu::~ContextMenu()
151{
152 // del any submenu
153 delete pSubmenu; pSubmenu = nullptr;
154 // forward RemoveElement to screen
155 Screen *pScreen = GetScreen();
156 if (pScreen) pScreen->RemoveElement(pChild: this);
157 // clear key bindings
158 delete pKeySelUp;
159 delete pKeySelDown;
160 delete pKeySubmenu;
161 delete pKeyBack;
162 delete pKeyAbort;
163 delete pKeyConfirm;
164 delete pKeyHotkey;
165 // clear children to get appropriate callbacks
166 Clear();
167}
168
169void ContextMenu::Abort(bool fByUser)
170{
171 // effect
172 if (fByUser) GUISound(szSound: "DoorClose");
173 // simply del menu: dtor will remove itself
174 delete this;
175}
176
177void ContextMenu::DrawElement(C4FacetEx &cgo)
178{
179 // draw context menu bg
180 lpDDraw->DrawBoxDw(sfcDest: cgo.Surface, iX1: rcBounds.x + cgo.TargetX, iY1: rcBounds.y + cgo.TargetY,
181 iX2: rcBounds.x + rcBounds.Wdt + cgo.TargetX - 1, iY2: rcBounds.y + rcBounds.Hgt + cgo.TargetY - 1,
182 C4GUI_ContextBGColor);
183 // context bg: mark selected item
184 if (pSelectedItem)
185 {
186 // get marked item bounds
187 C4Rect rcSelArea = pSelectedItem->GetBounds();
188 // do indent
189 rcSelArea.x += GetClientRect().x;
190 rcSelArea.y += GetClientRect().y;
191 // draw
192 lpDDraw->DrawBoxDw(sfcDest: cgo.Surface, iX1: rcSelArea.x + cgo.TargetX, iY1: rcSelArea.y + cgo.TargetY,
193 iX2: rcSelArea.x + rcSelArea.Wdt + cgo.TargetX - 1, iY2: rcSelArea.y + rcSelArea.Hgt + cgo.TargetY - 1,
194 C4GUI_ContextSelColor);
195 }
196 // draw frame
197 Draw3DFrame(cgo);
198}
199
200void ContextMenu::MouseInput(CMouse &rMouse, int32_t iButton, int32_t iX, int32_t iY, uint32_t dwKeyParam)
201{
202 // inherited
203 Window::MouseInput(rMouse, iButton, iX, iY, dwKeyParam);
204 // mouse is in client area?
205 if (GetClientRect().Contains(iX: iX + rcBounds.x, iY: iY + rcBounds.y))
206 {
207 // reset selection
208 Element *pPrevSelectedItem = pSelectedItem;
209 pSelectedItem = nullptr;
210 // get client component the mouse is over
211 iX -= GetMarginLeft(); iY -= GetMarginTop();
212 for (Element *pCurr = GetFirst(); pCurr; pCurr = pCurr->GetNext())
213 if (pCurr->GetBounds().Contains(iX, iY))
214 pSelectedItem = pCurr;
215 // selection change sound
216 if (pSelectedItem != pPrevSelectedItem)
217 {
218 SelectionChanged(fByUser: true);
219 // selection by mouse: Check whether submenu can be opened
220 CheckOpenSubmenu();
221 }
222 // check mouse click
223 if (iButton == C4MC_Button_LeftDown)
224 {
225 DoOK(); return;
226 }
227 }
228}
229
230void ContextMenu::MouseLeaveEntry(CMouse &rMouse, Entry *pOldEntry)
231{
232 // no submenu open? then deselect any selected item
233 if (pOldEntry == pSelectedItem && !pSubmenu)
234 {
235 pSelectedItem = nullptr;
236 SelectionChanged(fByUser: true);
237 }
238}
239
240bool ContextMenu::KeySelUp()
241{
242 // not if focus is in submenu
243 if (pSubmenu) return false;
244 Element *pPrevSelectedItem = pSelectedItem;
245 // select prev
246 if (pSelectedItem) pSelectedItem = pSelectedItem->GetPrev();
247 // nothing selected or beginning reached: cycle
248 if (!pSelectedItem) pSelectedItem = GetLastContained();
249 // selection might have changed
250 if (pSelectedItem != pPrevSelectedItem) SelectionChanged(fByUser: true);
251 return true;
252}
253
254bool ContextMenu::KeySelDown()
255{
256 // not if focus is in submenu
257 if (pSubmenu) return false;
258 Element *pPrevSelectedItem = pSelectedItem;
259 // select next
260 if (pSelectedItem) pSelectedItem = pSelectedItem->GetNext();
261 // nothing selected or end reached: cycle
262 if (!pSelectedItem) pSelectedItem = GetFirstContained();
263 // selection might have changed
264 if (pSelectedItem != pPrevSelectedItem) SelectionChanged(fByUser: true);
265 return true;
266}
267
268bool ContextMenu::KeySubmenu()
269{
270 // not if focus is in submenu
271 if (pSubmenu) return false;
272 CheckOpenSubmenu();
273 return true;
274}
275
276bool ContextMenu::KeyBack()
277{
278 // not if focus is in submenu
279 if (pSubmenu) return false;
280 // close submenu on keyboard input
281 if (IsSubmenu()) { Abort(fByUser: true); return true; }
282 return false;
283}
284
285bool ContextMenu::KeyAbort()
286{
287 // not if focus is in submenu
288 if (pSubmenu) return false;
289 Abort(fByUser: true);
290 return true;
291}
292
293bool ContextMenu::KeyConfirm()
294{
295 // not if focus is in submenu
296 if (pSubmenu) return false;
297 CheckOpenSubmenu();
298 DoOK();
299 return true;
300}
301
302bool ContextMenu::KeyHotkey(C4KeyCodeEx key)
303{
304 // not if focus is in submenu
305 if (pSubmenu) return false;
306 Element *pPrevSelectedItem = pSelectedItem;
307 C4KeyCode wKey = TOUPPERIFX11(key.Key);
308 if (Inside<C4KeyCode>(ival: wKey, lbound: 'A', rbound: 'Z') || Inside<C4KeyCode>(ival: wKey, lbound: '0', rbound: '9'))
309 {
310 // process hotkeys
311 char ch = char(wKey);
312 for (Element *pCurr = GetFirst(); pCurr; pCurr = pCurr->GetNext())
313 if (pCurr->OnHotkey(cHotkey: ch))
314 {
315 pSelectedItem = pCurr;
316 if (pSelectedItem != pPrevSelectedItem) SelectionChanged(fByUser: true);
317 CheckOpenSubmenu();
318 DoOK();
319 return true;
320 }
321 return false;
322 }
323 // unrecognized hotkey
324 return false;
325}
326
327void ContextMenu::UpdateElementPositions()
328{
329 // first item at zero offset
330 Element *pCurr = GetFirst();
331 if (!pCurr) return;
332 pCurr->GetBounds().y = 0;
333 int32_t iMinWdt = std::max<int32_t>(a: 20, b: pCurr->GetBounds().Wdt);
334 int32_t iOverallHgt = pCurr->GetBounds().Hgt;
335 // others stacked under it
336 while (pCurr = pCurr->GetNext())
337 {
338 iMinWdt = (std::max)(a: iMinWdt, b: pCurr->GetBounds().Wdt);
339 int32_t iYSpace = pCurr->GetListItemTopSpacing();
340 int32_t iNewY = iOverallHgt + iYSpace;
341 iOverallHgt += pCurr->GetBounds().Hgt + iYSpace;
342 if (iNewY != pCurr->GetBounds().y)
343 {
344 pCurr->GetBounds().y = iNewY;
345 pCurr->UpdateOwnPos();
346 }
347 }
348 // don't make smaller
349 iMinWdt = (std::max)(a: iMinWdt, b: rcBounds.Wdt - GetMarginLeft() - GetMarginRight());
350 // all entries same size
351 for (pCurr = GetFirst(); pCurr; pCurr = pCurr->GetNext())
352 if (pCurr->GetBounds().Wdt != iMinWdt)
353 {
354 pCurr->GetBounds().Wdt = iMinWdt;
355 pCurr->UpdateOwnPos();
356 }
357 // update own size
358 rcBounds.Wdt = iMinWdt + GetMarginLeft() + GetMarginRight();
359 rcBounds.Hgt = std::max<int32_t>(a: iOverallHgt, b: 8) + GetMarginTop() + GetMarginBottom();
360 UpdateSize();
361}
362
363void ContextMenu::RemoveElement(Element *pChild)
364{
365 // inherited
366 Window::RemoveElement(pChild);
367 // target lost?
368 if (pChild == pTarget) { Abort(fByUser: false); return; }
369 // submenu?
370 if (pChild == pSubmenu) pSubmenu = nullptr;
371 // clear selection var
372 if (pChild == pSelectedItem)
373 {
374 pSelectedItem = nullptr;
375 SelectionChanged(fByUser: false);
376 }
377 // forward to any submenu
378 if (pSubmenu) pSubmenu->RemoveElement(pChild);
379 // forward to mouse
380 if (GetScreen())
381 GetScreen()->Mouse.RemoveElement(pChild);
382 // update positions
383 UpdateElementPositions();
384}
385
386bool ContextMenu::AddElement(Element *pChild)
387{
388 // add it
389 Window::AddElement(pChild);
390 // update own size and positions
391 UpdateElementPositions();
392 // success
393 return true;
394}
395
396void ContextMenu::ElementSizeChanged(Element *pOfElement)
397{
398 // inherited
399 Window::ElementSizeChanged(pOfElement);
400 // update positions of all list items
401 UpdateElementPositions();
402}
403
404void ContextMenu::ElementPosChanged(Element *pOfElement)
405{
406 // inherited
407 Window::ElementSizeChanged(pOfElement);
408 // update positions of all list items
409 UpdateElementPositions();
410}
411
412void ContextMenu::SelectionChanged(bool fByUser)
413{
414 // any selection?
415 if (pSelectedItem)
416 {
417 // effect
418 if (fByUser) GUISound(szSound: "Command");
419 }
420 // close any submenu from prev selection
421 if (pSubmenu) pSubmenu->Abort(fByUser: true);
422}
423
424Screen *ContextMenu::GetScreen()
425{
426 // context menus don't have a parent; get screen by static var
427 return Screen::GetScreenS();
428}
429
430bool ContextMenu::CtxMouseInput(CMouse &rMouse, int32_t iButton, int32_t iScreenX, int32_t iScreenY, uint32_t dwKeyParam)
431{
432 // check submenu
433 if (pSubmenu)
434 if (pSubmenu->CtxMouseInput(rMouse, iButton, iScreenX, iScreenY, dwKeyParam)) return true;
435 // check bounds
436 if (!rcBounds.Contains(iX: iScreenX, iY: iScreenY)) return false;
437 // inside menu: do input in local coordinates
438 MouseInput(rMouse, iButton, iX: iScreenX - rcBounds.x, iY: iScreenY - rcBounds.y, dwKeyParam);
439 return true;
440}
441
442bool ContextMenu::CharIn(const char *c)
443{
444 // forward to submenu
445 if (pSubmenu) return pSubmenu->CharIn(c);
446 return false;
447}
448
449void ContextMenu::Draw(C4FacetEx &cgo)
450{
451 // draw self
452 Window::Draw(cgo);
453 // draw submenus on top
454 if (pSubmenu) pSubmenu->Draw(cgo);
455}
456
457void ContextMenu::Open(Element *pTarget, int32_t iScreenX, int32_t iScreenY)
458{
459 // set pos
460 rcBounds.x = iScreenX; rcBounds.y = iScreenY;
461 UpdatePos();
462 // set target
463 this->pTarget = pTarget;
464 // effect :)
465 GUISound(szSound: "DoorOpen");
466 // done
467}
468
469void ContextMenu::CheckOpenSubmenu()
470{
471 // safety
472 if (!GetScreen()) return;
473 // anything selected?
474 if (!pSelectedItem) return;
475 // get as entry
476 Entry *pSelEntry = static_cast<Entry *>(pSelectedItem);
477 // has submenu handler?
478 ContextHandler *pSubmenuHandler = pSelEntry->pSubmenuHandler;
479 if (!pSubmenuHandler) return;
480 // create submenu then
481 if (pSubmenu) pSubmenu->Abort(fByUser: false);
482 pSubmenu = pSubmenuHandler->OnSubcontext(pOnElement: pTarget);
483 // get open pos
484 int32_t iX = GetClientRect().x + pSelEntry->GetBounds().x + pSelEntry->GetBounds().Wdt;
485 int32_t iY = GetClientRect().y + pSelEntry->GetBounds().y + pSelEntry->GetBounds().Hgt / 2;
486 int32_t iScreenWdt = GetScreen()->GetBounds().Wdt, iScreenHgt = GetScreen()->GetBounds().Hgt;
487 if (iY + pSubmenu->GetBounds().Hgt >= iScreenHgt)
488 {
489 // bottom too narrow: open to top, if height is sufficient
490 // otherwise, open to top from bottom screen pos
491 if (iY < pSubmenu->GetBounds().Hgt) iY = iScreenHgt;
492 iY -= pSubmenu->GetBounds().Hgt;
493 }
494 if (iX + pSubmenu->GetBounds().Wdt >= iScreenWdt)
495 {
496 // right too narrow: try opening left of this menu
497 // otherwise, open to left from right screen border
498 if (GetClientRect().x < pSubmenu->GetBounds().Wdt)
499 iX = iScreenWdt;
500 else
501 iX = GetClientRect().x;
502 iX -= pSubmenu->GetBounds().Wdt;
503 }
504 // open it
505 pSubmenu->Open(pTarget, iScreenX: iX, iScreenY: iY);
506}
507
508bool ContextMenu::IsSubmenu()
509{
510 return GetScreen() && GetScreen()->pContext != this;
511}
512
513void ContextMenu::DoOK()
514{
515 // safety
516 if (!GetScreen()) return;
517 // anything selected?
518 if (!pSelectedItem) return;
519 // get as entry
520 Entry *pSelEntry = static_cast<Entry *>(pSelectedItem);
521 // get CB; take over pointer
522 MenuHandler *pCallback = pSelEntry->GetAndZeroCallback();
523 Element *pTarget = this->pTarget;
524 if (!pCallback) return;
525 // close all menus (deletes this class!) w/o sound
526 GetScreen()->AbortContext(fByUser: false);
527 // sound
528 GUISound(szSound: "Click");
529 // do CB
530 pCallback->OnOK(pTarget);
531 // free CB class
532 delete pCallback;
533}
534
535} // end of namespace
536