/* * OpenClonk, http://www.openclonk.org * * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/ * Copyright (c) 2009-2016, The OpenClonk Team and contributors * * Distributed under the terms of the ISC license; see accompanying file * "COPYING" for details. * * "Clonk" is a registered trademark of Matthes Bender, used with permission. * See accompanying file "TRADEMARK" for details. * * To redistribute this file separately, substitute the full license texts * for the above references. */ // generic user interface // context menu #include "C4Include.h" #include "gui/C4Gui.h" #include "graphics/C4Draw.h" #include "graphics/C4FacetEx.h" #include "graphics/C4GraphicsResource.h" #include "gui/C4MouseControl.h" namespace C4GUI { int32_t ContextMenu::iGlobalMenuIndex = 0; // ---------------------------------------------------- // ContextMenu::Entry void ContextMenu::Entry::DrawElement(C4TargetFacet &cgo) { // icon if (icoIcon > Ico_None) { // get icon counts int32_t iXMax, iYMax; ::GraphicsResource.fctIcons.GetPhaseNum(iXMax, iYMax); if (!iXMax) iXMax = 6; // load icon const C4Facet &rfctIcon = ::GraphicsResource.fctIcons.GetPhase(icoIcon % iXMax, icoIcon / iXMax); rfctIcon.DrawX(cgo.Surface, rcBounds.x + cgo.TargetX, rcBounds.y + cgo.TargetY, rcBounds.Hgt, rcBounds.Hgt); } // print out label if (!!sText) pDraw->TextOut(sText.getData(), ::GraphicsResource.TextFont, 1.0f, cgo.Surface, cgo.TargetX+rcBounds.x+GetIconIndent(), rcBounds.y + cgo.TargetY, C4GUI_ContextFontClr, ALeft); // submenu arrow if (pSubmenuHandler) { C4Facet &rSubFct = ::GraphicsResource.fctSubmenu; rSubFct.Draw(cgo.Surface, cgo.TargetX+rcBounds.x+rcBounds.Wdt - rSubFct.Wdt, cgo.TargetY+rcBounds.y+(rcBounds.Hgt - rSubFct.Hgt)/2); } } ContextMenu::Entry::Entry(const char *szText, Icons icoIcon, MenuHandler *pMenuHandler, ContextHandler *pSubmenuHandler) : Element(), cHotkey(0), icoIcon(icoIcon), pMenuHandler(pMenuHandler), pSubmenuHandler(pSubmenuHandler) { // set text with hotkey if (szText) { sText.Copy(szText); ExpandHotkeyMarkup(sText, cHotkey); // adjust size ::GraphicsResource.TextFont.GetTextExtent(sText.getData(), rcBounds.Wdt, rcBounds.Hgt, true); } else { rcBounds.Wdt = 40; rcBounds.Hgt = ::GraphicsResource.TextFont.GetLineHeight(); } // regard icon rcBounds.Wdt += GetIconIndent(); // submenu arrow if (pSubmenuHandler) rcBounds.Wdt += ::GraphicsResource.fctSubmenu.Wdt+2; } // ---------------------------------------------------- // ContextMenu ContextMenu::ContextMenu() : Window() { iMenuIndex = ++iGlobalMenuIndex; // set min size rcBounds.Wdt=40; rcBounds.Hgt=7; // key bindings C4CustomKey::CodeList Keys; Keys.emplace_back(K_UP); if (Config.Controls.GamepadGuiControl) { ControllerKeys::Up(Keys); } pKeySelUp = new C4KeyBinding(Keys, "GUIContextSelUp", KEYSCOPE_Gui, new C4KeyCB(*this, &ContextMenu::KeySelUp), C4CustomKey::PRIO_Context); Keys.clear(); Keys.emplace_back(K_DOWN); if (Config.Controls.GamepadGuiControl) { ControllerKeys::Down(Keys); } pKeySelDown = new C4KeyBinding(Keys, "GUIContextSelDown", KEYSCOPE_Gui, new C4KeyCB(*this, &ContextMenu::KeySelDown), C4CustomKey::PRIO_Context); Keys.clear(); Keys.emplace_back(K_RIGHT); if (Config.Controls.GamepadGuiControl) { ControllerKeys::Right(Keys); } pKeySubmenu = new C4KeyBinding(Keys, "GUIContextSubmenu", KEYSCOPE_Gui, new C4KeyCB(*this, &ContextMenu::KeySubmenu), C4CustomKey::PRIO_Context); Keys.clear(); Keys.emplace_back(K_LEFT); if (Config.Controls.GamepadGuiControl) { ControllerKeys::Left(Keys); } pKeyBack = new C4KeyBinding(Keys, "GUIContextBack", KEYSCOPE_Gui, new C4KeyCB(*this, &ContextMenu::KeyBack), C4CustomKey::PRIO_Context); Keys.clear(); Keys.emplace_back(K_ESCAPE); if (Config.Controls.GamepadGuiControl) { ControllerKeys::Cancel(Keys); } pKeyAbort = new C4KeyBinding(Keys, "GUIContextAbort", KEYSCOPE_Gui, new C4KeyCB(*this, &ContextMenu::KeyAbort), C4CustomKey::PRIO_Context); Keys.clear(); Keys.emplace_back(K_RETURN); if (Config.Controls.GamepadGuiControl) { ControllerKeys::Ok(Keys); } pKeyConfirm = new C4KeyBinding(Keys, "GUIContextConfirm", KEYSCOPE_Gui, new C4KeyCB(*this, &ContextMenu::KeyConfirm), C4CustomKey::PRIO_Context); pKeyHotkey = new C4KeyBinding(C4KeyCodeEx(KEY_Any), "GUIContextHotkey", KEYSCOPE_Gui, new C4KeyCBPassKey(*this, &ContextMenu::KeyHotkey), C4CustomKey::PRIO_Context); } ContextMenu::~ContextMenu() { // del any submenu if (pSubmenu) { delete pSubmenu; pSubmenu=nullptr; } // forward RemoveElement to screen Screen *pScreen = GetScreen(); if (pScreen) pScreen->RemoveElement(this); // clear key bindings delete pKeySelUp; delete pKeySelDown; delete pKeySubmenu; delete pKeyBack; delete pKeyAbort; delete pKeyConfirm; delete pKeyHotkey; // clear children to get appropriate callbacks Clear(); } void ContextMenu::Abort(bool fByUser) { // effect if (fByUser) GUISound("UI::Close"); // simply del menu: dtor will remove itself delete this; } void ContextMenu::DrawElement(C4TargetFacet &cgo) { // draw context menu bg pDraw->DrawBoxDw(cgo.Surface, rcBounds.x+cgo.TargetX, rcBounds.y+cgo.TargetY, rcBounds.x+rcBounds.Wdt+cgo.TargetX-1, rcBounds.y+rcBounds.Hgt+cgo.TargetY-1, C4GUI_ContextBGColor); // context bg: mark selected item if (pSelectedItem) { // get marked item bounds C4Rect rcSelArea = pSelectedItem->GetBounds(); // do indent rcSelArea.x += GetClientRect().x; rcSelArea.y += GetClientRect().y; // draw pDraw->DrawBoxDw(cgo.Surface, rcSelArea.x+cgo.TargetX, rcSelArea.y+cgo.TargetY, rcSelArea.x+rcSelArea.Wdt+cgo.TargetX-1, rcSelArea.y+rcSelArea.Hgt+cgo.TargetY-1, C4GUI_ContextSelColor); } // draw frame Draw3DFrame(cgo); } void ContextMenu::MouseInput(CMouse &rMouse, int32_t iButton, int32_t iX, int32_t iY, DWORD dwKeyParam) { // inherited Window::MouseInput(rMouse, iButton, iX, iY, dwKeyParam); // mouse is in client area? if (GetClientRect().Contains(iX+rcBounds.x, iY+rcBounds.y)) { // reset selection Element *pPrevSelectedItem = pSelectedItem; pSelectedItem = nullptr; // get client component the mouse is over iX -= GetMarginLeft(); iY -= GetMarginTop(); for (Element *pCurr = GetFirst(); pCurr; pCurr = pCurr->GetNext()) if (pCurr->GetBounds().Contains(iX, iY)) pSelectedItem = pCurr; // selection change sound if (pSelectedItem != pPrevSelectedItem) { SelectionChanged(true); // selection by mouse: Check whether submenu can be opened CheckOpenSubmenu(); } // check mouse click if (iButton == C4MC_Button_LeftDown) { DoOK(); return; } } } void ContextMenu::MouseLeaveEntry(CMouse &rMouse, Entry *pOldEntry) { // no submenu open? then deselect any selected item if (pOldEntry==pSelectedItem && !pSubmenu) { pSelectedItem = nullptr; SelectionChanged(true); } } bool ContextMenu::KeySelUp() { // not if focus is in submenu if (pSubmenu) return false; Element *pPrevSelectedItem = pSelectedItem; // select prev if (pSelectedItem) pSelectedItem = pSelectedItem->GetPrev(); // nothing selected or beginning reached: cycle if (!pSelectedItem) pSelectedItem = GetLastContained(); // selection might have changed if (pSelectedItem != pPrevSelectedItem) SelectionChanged(true); return true; } bool ContextMenu::KeySelDown() { // not if focus is in submenu if (pSubmenu) return false; Element *pPrevSelectedItem = pSelectedItem; // select next if (pSelectedItem) pSelectedItem = pSelectedItem->GetNext(); // nothing selected or end reached: cycle if (!pSelectedItem) pSelectedItem = GetFirstContained(); // selection might have changed if (pSelectedItem != pPrevSelectedItem) SelectionChanged(true); return true; } bool ContextMenu::KeySubmenu() { // not if focus is in submenu if (pSubmenu) return false; CheckOpenSubmenu(); return true; } bool ContextMenu::KeyBack() { // not if focus is in submenu if (pSubmenu) return false; // close submenu on keyboard input if (IsSubmenu()) { Abort(true); return true; } return false; } bool ContextMenu::KeyAbort() { // not if focus is in submenu if (pSubmenu) return false; Abort(true); return true; } bool ContextMenu::KeyConfirm() { // not if focus is in submenu if (pSubmenu) return false; CheckOpenSubmenu(); DoOK(); return true; } bool ContextMenu::KeyHotkey(const C4KeyCodeEx &key) { // not if focus is in submenu if (pSubmenu) return false; Element *pPrevSelectedItem = pSelectedItem; StdStrBuf sKey = C4KeyCodeEx::KeyCode2String(key.Key, true, true); // do hotkey procs for standard alphanumerics only if (sKey.getLength() != 1) return false; WORD wKey = WORD(*sKey.getData()); if (Inside(wKey, 'A', 'Z') || Inside(wKey, '0', '9')) { // process hotkeys uint32_t ch = wKey; for (Element *pCurr = GetFirst(); pCurr; pCurr = pCurr->GetNext()) if (pCurr->OnHotkey(ch)) { pSelectedItem = pCurr; if (pSelectedItem != pPrevSelectedItem) SelectionChanged(true); CheckOpenSubmenu(); DoOK(); return true; } return false; } // unrecognized hotkey return false; } void ContextMenu::UpdateElementPositions() { // first item at zero offset Element *pCurr = GetFirst(); if (!pCurr) return; pCurr->GetBounds().y = 0; int32_t iMinWdt = std::max(20, pCurr->GetBounds().Wdt);; int32_t iOverallHgt = pCurr->GetBounds().Hgt; // others stacked under it while ((pCurr = pCurr->GetNext())) { iMinWdt = std::max(iMinWdt, pCurr->GetBounds().Wdt); int32_t iYSpace = pCurr->GetListItemTopSpacing(); int32_t iNewY = iOverallHgt + iYSpace; iOverallHgt += pCurr->GetBounds().Hgt + iYSpace; if (iNewY != pCurr->GetBounds().y) { pCurr->GetBounds().y = iNewY; pCurr->UpdateOwnPos(); } } // don't make smaller iMinWdt = std::max(iMinWdt, rcBounds.Wdt - GetMarginLeft() - GetMarginRight()); // all entries same size for (pCurr = GetFirst(); pCurr; pCurr = pCurr->GetNext()) if (pCurr->GetBounds().Wdt != iMinWdt) { pCurr->GetBounds().Wdt = iMinWdt; pCurr->UpdateOwnPos(); } // update own size rcBounds.Wdt = iMinWdt + GetMarginLeft() + GetMarginRight(); rcBounds.Hgt = std::max(iOverallHgt, 8) + GetMarginTop() + GetMarginBottom(); UpdateSize(); } void ContextMenu::RemoveElement(Element *pChild) { // inherited Window::RemoveElement(pChild); // target lost? if (pChild == pTarget) { Abort(false); return; } // submenu? if (pChild == pSubmenu) pSubmenu = nullptr; // clear selection var if (pChild == pSelectedItem) { pSelectedItem = nullptr; SelectionChanged(false); } // forward to any submenu if (pSubmenu) pSubmenu->RemoveElement(pChild); // forward to mouse if (GetScreen()) GetScreen()->Mouse.RemoveElement(pChild); // update positions UpdateElementPositions(); } bool ContextMenu::AddElement(Element *pChild) { // add it Window::AddElement(pChild); // update own size and positions UpdateElementPositions(); // success return true; } bool ContextMenu::InsertElement(Element *pChild, Element *pInsertBefore) { // insert it Window::InsertElement(pChild, pInsertBefore); // update own size and positions UpdateElementPositions(); // success return true; } void ContextMenu::ElementSizeChanged(Element *pOfElement) { // inherited Window::ElementSizeChanged(pOfElement); // update positions of all list items UpdateElementPositions(); } void ContextMenu::ElementPosChanged(Element *pOfElement) { // inherited Window::ElementSizeChanged(pOfElement); // update positions of all list items UpdateElementPositions(); } void ContextMenu::SelectionChanged(bool fByUser) { // any selection? if (pSelectedItem) { // effect if (fByUser) GUISound("UI::Select"); } // close any submenu from prev selection if (pSubmenu) pSubmenu->Abort(true); } Screen *ContextMenu::GetScreen() { // context menus don't have a parent; get screen by static var return Screen::GetScreenS(); } bool ContextMenu::CtxMouseInput(CMouse &rMouse, int32_t iButton, int32_t iScreenX, int32_t iScreenY, DWORD dwKeyParam) { // check submenu if (pSubmenu) if (pSubmenu->CtxMouseInput(rMouse, iButton, iScreenX, iScreenY, dwKeyParam)) return true; // check bounds if (!rcBounds.Contains(iScreenX, iScreenY)) return false; // inside menu: do input in local coordinates MouseInput(rMouse, iButton, iScreenX - rcBounds.x, iScreenY - rcBounds.y, dwKeyParam); return true; } bool ContextMenu::CharIn(const char * c) { // forward to submenu if (pSubmenu) return pSubmenu->CharIn(c); return false; } void ContextMenu::Draw(C4TargetFacet &cgo) { // In editor mode, the surface is not assigned // The menu is drawn directly by the dialogue, so just exit here. if (!cgo.Surface) return; // draw self Window::Draw(cgo); // draw submenus on top if (pSubmenu) pSubmenu->Draw(cgo); } void ContextMenu::Open(Element *pTarget, int32_t iScreenX, int32_t iScreenY) { // set pos rcBounds.x = iScreenX; rcBounds.y = iScreenY; UpdatePos(); // set target this->pTarget = pTarget; // effect :) GUISound("UI::Open"); // done } void ContextMenu::CheckOpenSubmenu() { // safety if (!GetScreen()) return; // anything selected? if (!pSelectedItem) return; // get as entry Entry *pSelEntry = (Entry *) pSelectedItem; // has submenu handler? ContextHandler *pSubmenuHandler = pSelEntry->pSubmenuHandler; if (!pSubmenuHandler) return; // create submenu then if (pSubmenu) pSubmenu->Abort(false); pSubmenu = pSubmenuHandler->OnSubcontext(pTarget); // get open pos int32_t iX = GetClientRect().x + pSelEntry->GetBounds().x + pSelEntry->GetBounds().Wdt; int32_t iY = GetClientRect().y + pSelEntry->GetBounds().y + pSelEntry->GetBounds().Hgt/2; int32_t iScreenWdt = GetScreen()->GetBounds().Wdt, iScreenHgt = GetScreen()->GetBounds().Hgt; if (iY + pSubmenu->GetBounds().Hgt >= iScreenHgt) { // bottom too narrow: open to top, if height is sufficient // otherwise, open to top from bottom screen pos if (iY < pSubmenu->GetBounds().Hgt) iY = iScreenHgt; iY -= pSubmenu->GetBounds().Hgt; } if (iX + pSubmenu->GetBounds().Wdt >= iScreenWdt) { // right too narrow: try opening left of this menu // otherwise, open to left from right screen border if (GetClientRect().x < pSubmenu->GetBounds().Wdt) iX = iScreenWdt; else iX = GetClientRect().x; iX -= pSubmenu->GetBounds().Wdt; } // open it pSubmenu->Open(pTarget, iX, iY); } bool ContextMenu::IsSubmenu() { return GetScreen() && GetScreen()->pContext!=this; } void ContextMenu::DoOK() { // safety if (!GetScreen()) return; // anything selected? if (!pSelectedItem) return; // get as entry Entry *pSelEntry = (Entry *) pSelectedItem; // get CB; take over pointer MenuHandler *pCallback = pSelEntry->GetAndZeroCallback(); Element *pTarget = this->pTarget; if (!pCallback) return; // close all menus (deletes this class!) w/o sound GetScreen()->AbortContext(false); // sound GUISound("UI::Click"); // do CB pCallback->OnOK(pTarget); // free CB class delete pCallback; } void ContextMenu::SelectItem(int32_t iIndex) { // get item to be selected (may be nullptr on purpose!) Element *pNewSelElement = GetElementByIndex(iIndex); if (pNewSelElement != pSelectedItem) return; // set new pSelectedItem = pNewSelElement; SelectionChanged(false); } // ---------------------------------------------------- // ContextButton ContextButton::ContextButton(C4Rect &rtBounds) : Control(rtBounds), iOpenMenu(0), fMouseOver(false) { RegisterContextKey(); } ContextButton::ContextButton(Element *pForEl, bool fAdd, int32_t iHIndent, int32_t iVIndent) : Control(C4Rect(0,0,0,0)), iOpenMenu(0), fMouseOver(false) { SetBounds(pForEl->GetToprightCornerRect(16, 16, iHIndent, iVIndent)); // copy context handler SetContextHandler(pForEl->GetContextHandler()); // add if desired Container *pCont; if (fAdd) if ((pCont = pForEl->GetContainer())) pCont->AddElement(this); RegisterContextKey(); } ContextButton::~ContextButton() { delete pKeyContext; } void ContextButton::RegisterContextKey() { // reg keys for pressing the context button C4CustomKey::CodeList ContextKeys; ContextKeys.emplace_back(K_RIGHT); ContextKeys.emplace_back(K_DOWN); ContextKeys.emplace_back(K_SPACE); ContextKeys.emplace_back(K_RIGHT, KEYS_Alt); ContextKeys.emplace_back(K_DOWN, KEYS_Alt); ContextKeys.emplace_back(K_SPACE, KEYS_Alt); pKeyContext = new C4KeyBinding(ContextKeys, "GUIContextButtonPress", KEYSCOPE_Gui, new ControlKeyCB(*this, &ContextButton::KeyContext), C4CustomKey::PRIO_Ctrl); } bool ContextButton::DoContext(int32_t iX, int32_t iY) { // get context pos if (iX<0) { iX = rcBounds.Wdt/2; iY = rcBounds.Hgt/2; } // do context ContextHandler *pCtx = GetContextHandler(); if (!pCtx) return false; if (!pCtx->OnContext(this, iX, iY)) return false; // store menu Screen *pScr = GetScreen(); if (!pScr) return false; iOpenMenu = pScr->GetContextMenuIndex(); // return whether all was successful return !!iOpenMenu; } void ContextButton::DrawElement(C4TargetFacet &cgo) { // recheck open menu Screen *pScr = GetScreen(); if (!pScr || (iOpenMenu != pScr->GetContextMenuIndex())) iOpenMenu = 0; // calc drawing bounds int32_t x0 = cgo.TargetX + rcBounds.x, y0 = cgo.TargetY + rcBounds.y; // draw button; down (phase 1) if a menu is open ::GraphicsResource.fctContext.Draw(cgo.Surface, x0, y0, iOpenMenu ? 1 : 0); // draw selection highlight if (HasDrawFocus() || (fMouseOver && IsInActiveDlg(false)) || iOpenMenu) { pDraw->SetBlitMode(C4GFXBLIT_ADDITIVE); ::GraphicsResource.fctButtonHighlight.DrawX(cgo.Surface, x0, y0, rcBounds.Wdt, rcBounds.Hgt); pDraw->ResetBlitMode(); } } void ContextButton::MouseInput(CMouse &rMouse, int32_t iButton, int32_t iX, int32_t iY, DWORD dwKeyParam) { // left-click activates menu if ((iButton == C4MC_Button_LeftDown) || (iButton == C4MC_Button_RightDown)) if (DoContext()) return; // inherited Control::MouseInput(rMouse, iButton, iX, iY, dwKeyParam); } void ContextButton::MouseEnter(CMouse &rMouse) { Control::MouseEnter(rMouse); // remember mouse state for button highlight fMouseOver = true; } void ContextButton::MouseLeave(CMouse &rMouse) { Control::MouseLeave(rMouse); // mouse left fMouseOver = false; } } // end of namespace