1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2001, Sven2
6 * Copyright (c) 2017-2021, 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// dropdown box
20// implemented via context menu
21
22#include "C4GuiComboBox.h"
23#include "C4GuiResource.h"
24#include <C4Include.h>
25#include <C4Gui.h>
26#include <C4FacetEx.h>
27#include <C4Wrappers.h>
28#include <C4MouseControl.h>
29
30#include <StdWindow.h>
31
32namespace C4GUI
33{
34
35// ComboBox_FillCB
36
37void ComboBox_FillCB::AddEntry(const char *szText, int32_t id, const char *desc)
38{
39 if (!szText) szText = "";
40 typedef C4GUI::CBMenuHandlerEx<ComboBox, ComboBox::ComboMenuCBStruct> Handler;
41 Handler *pHandler = new Handler(pCombo, &ComboBox::OnCtxComboSelect);
42 pHandler->SetExtra(ComboBox::ComboMenuCBStruct(szText, id));
43 pDrop->AddItem(szText, szToolTip: desc ? desc : LoadResStr(id: C4ResStrTableKey::IDS_MSG_SELECT, args&: szText).c_str(), icoIcon: Ico_Empty, pMenuHandler: pHandler);
44}
45
46bool ComboBox_FillCB::FindEntry(const char *szText)
47{
48 // check for entry with same name
49 ContextMenu::Entry *pEntry; int32_t idx = 0;
50 while (pEntry = pDrop->GetIndexedEntry(iIndex: idx++)) if (SEqual(szStr1: pEntry->GetText(), szStr2: szText)) return true;
51 return false;
52}
53
54void ComboBox_FillCB::ClearEntries()
55{
56 pDrop->Clear();
57}
58
59// ComboBox
60
61ComboBox::ComboBox(const C4Rect &rtBounds) :
62 Control(rtBounds), iOpenMenu(0), pFillCallback(nullptr), fReadOnly(false), fSimple(false), fMouseOver(false),
63 pUseFont(nullptr), dwFontClr(C4GUI_ComboFontClr), dwBGClr(C4GUI_StandardBGColor), dwBorderClr(0), pFctSideArrow(nullptr)
64{
65 *Text = 0;
66 // key callbacks - lots of possibilities to get the dropdown
67 C4CustomKey::CodeList cbKeys;
68 cbKeys.push_back(x: C4KeyCodeEx(K_DOWN));
69 cbKeys.push_back(x: C4KeyCodeEx(K_SPACE));
70 cbKeys.push_back(x: C4KeyCodeEx(K_DOWN, KEYS_Alt));
71 cbKeys.push_back(x: C4KeyCodeEx(K_SPACE, KEYS_Alt));
72 if (Config.Controls.GamepadGuiControl)
73 {
74 cbKeys.push_back(x: C4KeyCodeEx(KEY_Gamepad(idGamepad: 0, idButton: KEY_JOY_AnyLowButton)));
75 cbKeys.push_back(x: C4KeyCodeEx(KEY_Gamepad(idGamepad: 0, idButton: KEY_JOY_Down)));
76 }
77 pKeyOpenCombo = new C4KeyBinding(cbKeys, "GUIComboOpen", KEYSCOPE_Gui,
78 new ControlKeyCB<ComboBox>(*this, &ComboBox::KeyDropDown), C4CustomKey::PRIO_Ctrl);
79 cbKeys.clear();
80 cbKeys.push_back(x: C4KeyCodeEx(K_ESCAPE));
81 if (Config.Controls.GamepadGuiControl)
82 {
83 cbKeys.push_back(x: C4KeyCodeEx(KEY_Gamepad(idGamepad: 0, idButton: KEY_JOY_AnyHighButton)));
84 }
85 pKeyCloseCombo = new C4KeyBinding(cbKeys, "GUIComboClose", KEYSCOPE_Gui,
86 new ControlKeyCB<ComboBox>(*this, &ComboBox::KeyAbortDropDown), C4CustomKey::PRIO_Ctrl);
87}
88
89ComboBox::~ComboBox()
90{
91 delete pKeyCloseCombo;
92 delete pKeyOpenCombo;
93 delete pFillCallback;
94}
95
96void ComboBox::SetComboCB(ComboBox_FillCB *pNewFillCallback)
97{
98 delete pFillCallback;
99 pFillCallback = pNewFillCallback;
100}
101
102bool ComboBox::DoDropdown()
103{
104 // not if readonly
105 if (fReadOnly) return false;
106 // get dropdown pos
107 int32_t iX = 0;
108 int32_t iY = rcBounds.Hgt;
109 // do dropdown
110 Screen *pScreen = GetScreen();
111 if (!pScreen) return false;
112 // item list as context menu
113 if (!pFillCallback) return false;
114 ContextMenu *pNewMenu = new C4GUI::ContextMenu();
115 // init with minimum size
116 pNewMenu->GetBounds().Wdt = (std::max)(a: rcBounds.Wdt, b: pNewMenu->GetBounds().Wdt);
117 // fill with items
118 pFillCallback->FillDropDown(pComboBox: this, pDropdownList: pNewMenu);
119 // open it on screen
120 pScreen->DoContext(pNewCtx: pNewMenu, pAtElement: this, iX, iY);
121 // store menu
122 iOpenMenu = pNewMenu->GetMenuIndex();
123 // done, success
124 return true;
125}
126
127bool ComboBox::AbortDropdown(bool fByUser)
128{
129 // recheck open menu
130 Screen *pScr = GetScreen();
131 if (!pScr || (iOpenMenu != pScr->GetLastContextMenuIndex())) iOpenMenu = 0;
132 if (!iOpenMenu) return false;
133 // abort it
134 pScr->AbortContext(fByUser);
135 return true;
136}
137
138void ComboBox::DrawElement(C4FacetEx &cgo)
139{
140 CStdFont *pUseFont = this->pUseFont ? this->pUseFont : &(GetRes()->TextFont);
141 // recheck open menu
142 Screen *pScr = GetScreen();
143 if (!pScr || (iOpenMenu != pScr->GetContextMenuIndex())) iOpenMenu = 0;
144 // calc drawing bounds
145 int32_t x0 = cgo.TargetX + rcBounds.x, y0 = cgo.TargetY + rcBounds.y;
146 int32_t iRightTextEnd = x0 + rcBounds.Wdt - GetRes()->fctContext.Wdt - 1;
147 if (!fReadOnly && !fSimple)
148 {
149 // draw background
150 lpDDraw->DrawBoxDw(sfcDest: cgo.Surface, iX1: x0, iY1: y0, iX2: x0 + rcBounds.Wdt - 1, iY2: y0 + rcBounds.Hgt - 1, dwClr: dwBGClr);
151 // draw frame
152 if (dwBorderClr)
153 {
154 int32_t x1 = cgo.TargetX + rcBounds.x, y1 = cgo.TargetY + rcBounds.y, x2 = x1 + rcBounds.Wdt, y2 = y1 + rcBounds.Hgt;
155 lpDDraw->DrawFrameDw(sfcDest: cgo.Surface, x1, y1, x2, y2: y2 - 1, dwClr: dwBorderClr);
156 lpDDraw->DrawFrameDw(sfcDest: cgo.Surface, x1: x1 + 1, y1: y1 + 1, x2: x2 - 1, y2: y2 - 2, dwClr: dwBorderClr);
157 }
158 else
159 // default frame color
160 Draw3DFrame(cgo);
161 // draw button; down (phase 1) if combo is down
162 (pFctSideArrow ? pFctSideArrow : &(GetRes()->fctContext))->Draw(sfcTarget: cgo.Surface, iX: iRightTextEnd, iY: y0 + (rcBounds.Hgt - GetRes()->fctContext.Hgt) / 2, iPhaseX: iOpenMenu ? 1 : 0);
163 }
164 else if (!fReadOnly)
165 {
166 // draw button in simple mode: Left of text
167 (pFctSideArrow ? pFctSideArrow : &(GetRes()->fctContext))->Draw(sfcTarget: cgo.Surface, iX: x0, iY: y0 + (rcBounds.Hgt - GetRes()->fctContext.Hgt) / 2, iPhaseX: iOpenMenu ? 1 : 0);
168 }
169 // draw text
170 if (*Text)
171 {
172 int clx1, cly1, clx2, cly2;
173 lpDDraw->GetPrimaryClipper(rX1&: clx1, rY1&: cly1, rX2&: clx2, rY2&: cly2);
174 lpDDraw->SubPrimaryClipper(iX1: x0, iY1: y0, iX2: iRightTextEnd - 1, iY2: y0 + rcBounds.Hgt - 1);
175 lpDDraw->TextOut(szText: Text, rFont&: *pUseFont, fZoom: 1.0f, sfcDest: cgo.Surface, iTx: x0 + GetRes()->fctContext.Wdt + 2, iTy: y0 + (rcBounds.Hgt - pUseFont->GetLineHeight()) / 2, dwFCol: dwFontClr, byForm: ALeft);
176 lpDDraw->SetPrimaryClipper(iX1: clx1, iY1: cly1, iX2: clx2, iY2: cly2);
177 }
178 // draw selection highlight
179 if ((HasDrawFocus() || iOpenMenu || fMouseOver) && !fReadOnly)
180 {
181 lpDDraw->SetBlitMode(C4GFXBLIT_ADDITIVE);
182 GetRes()->fctButtonHighlight.DrawX(sfcTarget: cgo.Surface, iX: x0, iY: y0, iWdt: rcBounds.Wdt, iHgt: rcBounds.Hgt);
183 lpDDraw->ResetBlitMode();
184 }
185}
186
187void ComboBox::MouseInput(CMouse &rMouse, int32_t iButton, int32_t iX, int32_t iY, uint32_t dwKeyParam)
188{
189 // left-click activates menu
190 if (!fReadOnly) if (iButton == C4MC_Button_LeftDown)
191 {
192 // recheck open menu
193 Screen *pScr = GetScreen();
194 if (!pScr || (iOpenMenu != pScr->GetLastContextMenuIndex())) iOpenMenu = 0;
195 if (iOpenMenu)
196 // left-click with combo down: abort has been done by screen; ignore
197 return;
198 else
199 // otherwise, open it
200 if (DoDropdown()) return;
201 }
202 // inherited
203 Control::MouseInput(rMouse, iButton, iX, iY, dwKeyParam);
204}
205
206void ComboBox::MouseEnter(CMouse &rMouse)
207{
208 fMouseOver = true;
209 Control::MouseEnter(rMouse);
210}
211
212void ComboBox::MouseLeave(CMouse &rMouse)
213{
214 fMouseOver = false;
215 Control::MouseLeave(rMouse);
216}
217
218int32_t ComboBox::GetDefaultHeight()
219{
220 return GetRes()->TextFont.GetLineHeight() + 4;
221}
222
223void ComboBox::SetText(const char *szToText)
224{
225 if (szToText) SCopy(szSource: szToText, sTarget: Text, iMaxL: C4MaxTitle); else *Text = 0;
226}
227
228void ComboBox::OnCtxComboSelect(C4GUI::Element *pListItem, const ComboMenuCBStruct &rNewSel)
229{
230 // ignore in readonly
231 if (fReadOnly) return;
232 // do callback
233 if (!pFillCallback || !pFillCallback->OnComboSelChange(pForCombo: this, idNewSelection: rNewSel.id))
234 // callback didn't process: default behaviour
235 SetText(rNewSel.sText.getData());
236 // don't do anything else, because this might be deleted
237}
238
239} // namespace C4GUI
240