openclonk/src/gui/C4GuiMenu.cpp

665 lines
19 KiB
C++

/*
* 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<ContextMenu>(*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<ContextMenu>(*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<ContextMenu>(*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<ContextMenu>(*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<ContextMenu>(*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<ContextMenu>(*this, &ContextMenu::KeyConfirm), C4CustomKey::PRIO_Context);
pKeyHotkey = new C4KeyBinding(C4KeyCodeEx(KEY_Any), "GUIContextHotkey", KEYSCOPE_Gui,
new C4KeyCBPassKey<ContextMenu>(*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<C4KeyCode, C4KeyCode, C4KeyCode>(wKey, 'A', 'Z') || Inside<C4KeyCode, C4KeyCode, C4KeyCode>(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<int32_t>(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<int32_t>(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<ContextButton>(*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