| 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 | |
| 32 | namespace C4GUI |
| 33 | { |
| 34 | |
| 35 | // ComboBox_FillCB |
| 36 | |
| 37 | void 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 | |
| 46 | bool 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 | |
| 54 | void ComboBox_FillCB::ClearEntries() |
| 55 | { |
| 56 | pDrop->Clear(); |
| 57 | } |
| 58 | |
| 59 | // ComboBox |
| 60 | |
| 61 | ComboBox::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 | |
| 89 | ComboBox::~ComboBox() |
| 90 | { |
| 91 | delete pKeyCloseCombo; |
| 92 | delete pKeyOpenCombo; |
| 93 | delete pFillCallback; |
| 94 | } |
| 95 | |
| 96 | void ComboBox::SetComboCB(ComboBox_FillCB *pNewFillCallback) |
| 97 | { |
| 98 | delete pFillCallback; |
| 99 | pFillCallback = pNewFillCallback; |
| 100 | } |
| 101 | |
| 102 | bool 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 * = 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 | |
| 127 | bool 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 | |
| 138 | void 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 | |
| 187 | void 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 | |
| 206 | void ComboBox::MouseEnter(CMouse &rMouse) |
| 207 | { |
| 208 | fMouseOver = true; |
| 209 | Control::MouseEnter(rMouse); |
| 210 | } |
| 211 | |
| 212 | void ComboBox::MouseLeave(CMouse &rMouse) |
| 213 | { |
| 214 | fMouseOver = false; |
| 215 | Control::MouseLeave(rMouse); |
| 216 | } |
| 217 | |
| 218 | int32_t ComboBox::GetDefaultHeight() |
| 219 | { |
| 220 | return GetRes()->TextFont.GetLineHeight() + 4; |
| 221 | } |
| 222 | |
| 223 | void ComboBox::SetText(const char *szToText) |
| 224 | { |
| 225 | if (szToText) SCopy(szSource: szToText, sTarget: Text, iMaxL: C4MaxTitle); else *Text = 0; |
| 226 | } |
| 227 | |
| 228 | void ComboBox::(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 | |