/* * OpenClonk, http://www.openclonk.org * * Copyright (c) 1998-2000, Matthes Bender * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/ * Copyright (c) 2009-2013, 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. */ /* In-game menu as used by objects, players, and fullscreen options */ #include #include #include #include #include #include #include #include #include #include #include const int32_t C4MN_InfoCaption_Delay = 90; // ----------------------------------------------------------- // C4MenuItem C4MenuItem::C4MenuItem(C4Menu *pMenu, int32_t iIndex, const char *szCaption, const char *szCommand, const char *szInfoCaption, int32_t iStyle) : C4GUI::Element(), pSymbolGraphics(NULL), dwSymbolClr(0u), fSelected(false), iStyle(iStyle), pMenu(pMenu), iIndex(iIndex) { *Caption=*Command=*InfoCaption=0; Symbol.Default(); SCopy(szCaption,Caption,C4MaxTitle); SCopy(szCommand,Command,_MAX_FNAME+30); SCopy(szInfoCaption,InfoCaption,C4MaxTitle); // some info caption corrections SReplaceChar(InfoCaption, 10, ' '); SReplaceChar(InfoCaption, 13, '|'); SetToolTip(InfoCaption); } C4MenuItem::~C4MenuItem() { Symbol.Clear(); } int32_t C4MenuItem::GetSymbolWidth(int32_t iForHeight) { // Context menus if (iStyle==C4MN_Style_Context) return std::max(Symbol.Wdt * iForHeight / std::max(Symbol.Hgt, 1.0f), static_cast(iForHeight)); // no symbol return 0; } void C4MenuItem::DrawElement(C4TargetFacet &cgo) { // get target pos C4Facet cgoOut(cgo.Surface, cgo.TargetX + rcBounds.x, cgo.TargetY + rcBounds.y, rcBounds.Wdt, rcBounds.Hgt); // Select mark if (fSelected) pDraw->DrawBoxDw(cgo.Surface, cgoOut.X, cgoOut.Y, cgoOut.X + cgoOut.Wdt - 1, cgoOut.Y + cgoOut.Hgt - 1, C4RGB(0xca, 0, 0)); // Symbol/text areas C4Facet cgoItemSymbol,cgoItemText; cgoItemSymbol=cgoItemText=cgoOut; int32_t iSymWidth; if ((iSymWidth = GetSymbolWidth(cgoItemText.Hgt))) { // get symbol area cgoItemSymbol=cgoItemText.Truncate(C4FCT_Left, iSymWidth); } // cgoItemSymbol.Hgt is 0. This means rcBounds.Hgt is 0. That // makes no sense at this point, so let's just draw in a // square area at item y. C4Facet cgoSymbolOut(cgoItemSymbol.Surface, cgoItemSymbol.X, cgoItemSymbol.Y, cgoItemSymbol.Wdt, cgoItemSymbol.Wdt); // Draw item symbol: if (pSymbolGraphics) { pSymbolGraphics->Draw(cgoSymbolOut, dwSymbolClr ? dwSymbolClr : 0xffffffff, NULL, 0, 0, NULL); } else if (Symbol.Surface) Symbol.DrawClr(cgoItemSymbol, true, dwSymbolClr); // Draw item text pDraw->StorePrimaryClipper(); pDraw->SubPrimaryClipper(cgoItemText.X, cgoItemText.Y, cgoItemText.X+cgoItemText.Wdt-1, cgoItemText.Y+cgoItemText.Hgt-1); if (iStyle == C4MN_Style_Context) pDraw->TextOut(Caption, ::GraphicsResource.FontRegular, 1.0f, cgoItemText.Surface, cgoItemText.X + C4GUI_IconLabelSpacing, cgoItemText.Y + round((cgoItemText.Hgt - ::GraphicsResource.FontRegular.GetLineHeight())/2)-1, C4GUI_ContextFontClr); pDraw->RestorePrimaryClipper(); } void C4MenuItem::MouseInput(C4GUI::CMouse &rMouse, int32_t iButton, int32_t iX, int32_t iY, DWORD dwKeyParam) { // clicky clicky! if (iButton == C4MC_Button_LeftUp || iButton == C4MC_Button_RightUp) { pMenu->UserEnter(::MouseControl.GetPlayer(), this); return; } // inherited; this is just setting some vars typedef C4GUI::Element ParentClass; ParentClass::MouseInput(rMouse, iButton, iX, iY, dwKeyParam); } void C4MenuItem::MouseEnter(C4GUI::CMouse &rMouse) { // callback to menu: Select item pMenu->UserSelectItem(::MouseControl.GetPlayer(), this); typedef C4GUI::Element ParentClass; ParentClass::MouseEnter(rMouse); } // ----------------------------------------------------------- // C4Menu C4Menu::C4Menu() : C4GUI::Dialog(100, 100, NULL, true) // will be re-adjusted later { Default(); AddElement(pClientWindow = new C4GUI::ScrollWindow(this)); // initially invisible: Will be made visible at first drawing by viewport // when the location will be inialized SetVisibility(false); } void C4Menu::Default() { Selection=-1; Style=C4MN_Style_Normal; ItemCount=0; ItemWidth=ItemHeight=C4SymbolSize; NeedRefill=false; Symbol.Default(); Caption[0]=0; Permanent=false; TimeOnSelection=0; Identification=0; LocationSet=false; Columns=Lines=0; Alignment= C4MN_Align_Right | C4MN_Align_Bottom; CloseCommand.Clear(); fActive = false; } void C4Menu::Clear() { Close(false); Symbol.Clear(); ClearItems(); ClearFrameDeco(); fActive = false; } bool C4Menu::TryClose(bool fOK, bool fControl) { // abort if menu is permanented by script if (!fOK) if (IsCloseDenied()) return false; // close the menu Close(fOK); Clear(); ::pGUI->RemoveElement(this); // invoke close command if (fControl && CloseCommand.getData()) { MenuCommand(CloseCommand.getData(), true); } // done return true; } bool C4Menu::DoInitRefSym(const C4Facet &fctSymbol, const char *szEmpty, int32_t iId, int32_t iStyle) { Clear(); Default(); Symbol.Set(fctSymbol); return InitMenu(szEmpty, iId, iStyle); } bool C4Menu::InitMenu(const char *szEmpty, int32_t iId, int32_t iStyle) { SCopy(szEmpty,Caption,C4MaxTitle); Identification=iId; if (*Caption) SetTitle(Caption, HasMouse()); else SetTitle(" ", HasMouse()); if (pTitle) pTitle->SetIcon(Symbol); Style=iStyle; // Menus are synchronous to allow COM_MenuUp/Down to be converted to movements at the clients if (Style == C4MN_Style_Normal) Columns = 5; else Columns=1; ::pGUI->ShowDialog(this, false); fActive = true; return true; } bool C4Menu::AddRefSym(const char *szCaption, const C4Facet &fctSymbol, const char *szCommand, const char *szInfoCaption) { if (!IsActive()) return false; // Create new menu item C4MenuItem *pNew = new C4MenuItem(this, ItemCount, szCaption,szCommand,szInfoCaption,Style); // Ref Symbol pNew->RefSymbol(fctSymbol); // Add return AddItem(pNew, szCaption, szCommand, szInfoCaption); } bool C4Menu::Add(const char *szCaption, C4FacetSurface &fctSymbol, const char *szCommand, const char *szInfoCaption) { if (!IsActive()) return false; // Create new menu item C4MenuItem *pNew = new C4MenuItem(this, ItemCount, szCaption,szCommand,szInfoCaption,Style); // Set Symbol pNew->GrabSymbol(fctSymbol); // Add return AddItem(pNew, szCaption, szCommand, szInfoCaption); } bool C4Menu::AddItem(C4MenuItem *pNew, const char *szCaption, const char *szCommand, const char *szInfoCaption) { // Add it to the list pClientWindow->AddElement(pNew); // Item count ItemCount++; // set new item size if (!pClientWindow->IsFrozen()) UpdateElementPositions(); // Init selection if not frozen if (Selection==-1 && !pClientWindow->IsFrozen()) SetSelection(ItemCount-1, false, false); // adjust scrolling, etc. UpdateScrollBar(); // Success return true; } bool C4Menu::Control(BYTE byCom, int32_t iData) { if (!IsActive()) return false; switch (byCom) { case COM_MenuEnter: Enter(); break; case COM_MenuClose: TryClose(false, true); break; // organize with nicer subfunction... case COM_MenuLeft: // Top wrap-around if (Selection-1 < 0) MoveSelection(ItemCount - 1 - Selection, true, true); else MoveSelection(-1, true, true); break; case COM_MenuRight: // Bottom wrap-around if (Selection+1 >= ItemCount) MoveSelection(-Selection, true, true); else MoveSelection(+1, true, true); break; case COM_MenuUp: iData = -Columns; // Top wrap-around if (Selection + iData < 0) while (Selection + iData + Columns < ItemCount) iData += Columns; MoveSelection(iData, true, true); break; case COM_MenuDown: iData = +Columns; // Bottom wrap-around if (Selection+iData >= ItemCount) while (Selection+iData-Columns >= 0) iData-=Columns; MoveSelection(iData, true, true); break; } return true; } bool C4Menu::KeyControl(BYTE byCom) { // direct keyboard callback if (!IsActive()) return false; return !!Control(byCom, 0); } bool C4Menu::IsActive() { return fActive; } bool C4Menu::Enter() { // Not active if (!IsActive()) return false; // Get selected item C4MenuItem *pItem = GetSelectedItem(); if (!pItem) return true; // Copy command to buffer (menu might be cleared) char szCommand[_MAX_FNAME+30+1]; SCopy(pItem->Command,szCommand); // Close if not permanent if (!Permanent) { Close(true); fActive = false; } // exec command (note that menu callback may delete this!) MenuCommand(szCommand, false); return true; } C4MenuItem* C4Menu::GetItem(int32_t iIndex) { return static_cast(pClientWindow->GetElementByIndex(iIndex)); } int32_t C4Menu::GetItemCount() { return ItemCount; } bool C4Menu::MoveSelection(int32_t iBy, bool fAdjustPosition, bool fDoCalls) { if (!iBy) return false; // find next item that can be selected by moving in iBy steps int32_t iNewSel = Selection; for (;;) { // determine new selection iNewSel += iBy; // selection is out of menu range if (!Inside(iNewSel, 0, ItemCount-1)) return false; // got something: go select it if (GetItem(iNewSel)) break; } // select it return !!SetSelection(iNewSel, fAdjustPosition, fDoCalls); } bool C4Menu::SetSelection(int32_t iSelection, bool fAdjustPosition, bool fDoCalls) { // Not active if (!IsActive()) return false; // Outside Limits / Selectable C4MenuItem *pNewSel = GetItem(iSelection); if ((iSelection==-1 && !ItemCount) || pNewSel) { // Selection change if (iSelection!=Selection) { // calls C4MenuItem *pSel = GetSelectedItem(); if (pSel) pSel->SetSelected(false); // Set selection Selection=iSelection; // Reset time on selection TimeOnSelection=0; } // always recheck selection for internal refill C4MenuItem *pSel = GetSelectedItem(); if (pSel) pSel->SetSelected(true); // set main caption by selection if (Style == C4MN_Style_Normal) { if (pSel) SetTitle(*(pSel->Caption) ? pSel->Caption : " ", HasMouse()); else SetTitle(*Caption ? Caption : " ", HasMouse()); } } // adjust position, if desired if (fAdjustPosition) AdjustPosition(); // do selection callback if (fDoCalls) OnSelectionChanged(Selection); // Done return true; } C4MenuItem* C4Menu::GetSelectedItem() { return GetItem(Selection); } void C4Menu::AdjustPosition() { // Adjust position by selection (works only after InitLocation) if ((Lines > 1) && Columns) { C4MenuItem *pSel = GetSelectedItem(); if (pSel) pClientWindow->ScrollRangeInView(pSel->GetBounds().y, pSel->GetBounds().Hgt); } } int32_t C4Menu::GetSelection() { return Selection; } bool C4Menu::SetPosition(int32_t iPosition) { if (!IsActive()) return false; // update scroll pos, if location is initialized if (IsVisible() && pClientWindow) pClientWindow->SetScroll((iPosition/Columns) * ItemHeight); return true; } int32_t C4Menu::GetIdentification() { return Identification; } void C4Menu::SetSize(int32_t iToWdt, int32_t iToHgt) { if (iToWdt) Columns=iToWdt; if (iToHgt) Lines=iToHgt; InitSize(); } void C4Menu::InitLocation(C4Facet &cgoArea) { ItemWidth=ItemHeight=C4SymbolSize; if (Style == C4MN_Style_Context) { ItemHeight = std::max(ItemHeight, ::GraphicsResource.FontRegular.GetLineHeight()); int32_t iWdt, iHgt; ::GraphicsResource.FontRegular.GetTextExtent(Caption, iWdt, iHgt, true); // FIXME: Blah. This stuff should be calculated correctly by pTitle. ItemWidth += iWdt + 20 + C4GUI_IconLabelSpacing; C4MenuItem *pItem; for (int i = 0; (pItem = GetItem(i)); ++i) { ::GraphicsResource.FontRegular.GetTextExtent(pItem->Caption, iWdt, iHgt, true); ItemWidth = std::max(ItemWidth, iWdt + pItem->GetSymbolWidth(ItemHeight) + C4GUI_IconLabelSpacing); } ItemWidth += 3; // Add some extra space so text doesn't touch right border frame... } Lines = ItemCount/Columns+std::min(ItemCount%Columns,1); // adjust by max. height Lines=std::max(std::min((cgoArea.Hgt-100)/std::max(ItemHeight,1),Lines),1); InitSize(); int32_t X,Y; X = (cgoArea.Wdt - rcBounds.Wdt)/2; Y = (cgoArea.Hgt - rcBounds.Hgt)/2; // Alignment if (Alignment & C4MN_Align_Left) X=C4SymbolSize; if (Alignment & C4MN_Align_Right) X=cgoArea.Wdt-2*C4SymbolSize-rcBounds.Wdt; if (Alignment & C4MN_Align_Top) Y=C4SymbolSize; if (Alignment & C4MN_Align_Bottom) Y=cgoArea.Hgt-C4SymbolSize-rcBounds.Hgt; // Centered (due to small viewport size) if (rcBounds.Wdt>cgoArea.Wdt-2*C4SymbolSize) X=(cgoArea.Wdt-rcBounds.Wdt)/2; if (rcBounds.Hgt>cgoArea.Hgt-2*C4SymbolSize) Y=(cgoArea.Hgt-rcBounds.Hgt)/2; SetPos(X, Y); // Position initialized: Make it visible to be used! SetVisibility(true); // now align all menu items correctly UpdateElementPositions(); // and correct scroll pos UpdateScrollBar(); AdjustPosition(); } void C4Menu::InitSize() { C4GUI::Element *pLast = pClientWindow->GetLast(); // Size calculation int Width, Height; Width=Columns*ItemWidth; Height=Lines*ItemHeight; bool fBarNeeded; fBarNeeded = pLast && pLast->GetBounds().y + pLast->GetBounds().Hgt > pClientWindow->GetBounds().Hgt; // add dlg margins Width += GetMarginLeft() + GetMarginRight() + pClientWindow->GetMarginLeft() + pClientWindow->GetMarginRight(); Height += GetMarginTop() + GetMarginBottom() + pClientWindow->GetMarginTop() + pClientWindow->GetMarginBottom(); if (fBarNeeded) Width += C4GUI_ScrollBarWdt; SetBounds(C4Rect(rcBounds.x, rcBounds.y, Width, Height)); pClientWindow->SetScrollBarEnabled(fBarNeeded); UpdateOwnPos(); } void C4Menu::UpdateScrollBar() { C4GUI::Element *pLast = pClientWindow->GetLast(); bool fBarNeeded = pLast && pLast->GetBounds().y + pLast->GetBounds().Hgt > pClientWindow->GetBounds().Hgt; if (pClientWindow->IsScrollBarEnabled() == fBarNeeded) return; // resize for bar InitSize(); } void C4Menu::Draw(C4TargetFacet &cgo) { // Inactive if (!IsActive()) return; // Location if (!LocationSet) { InitLocation(cgo); LocationSet=true; } // If drawn by a viewport, then it's made visible SetVisibility(true); // do drawing typedef C4GUI::Dialog ParentClass; ParentClass::Draw(cgo); // draw tooltip if selection time has been long enough ++TimeOnSelection; if (TimeOnSelection >= C4MN_InfoCaption_Delay && !::Control.isReplay() && !::pGUI->Mouse.IsActiveInput()) { C4MenuItem *pSel = GetSelectedItem(); if (pSel && *pSel->InfoCaption) { int32_t iX=0, iY=0; pSel->ClientPos2ScreenPos(iX, iY); C4GUI::Screen::DrawToolTip(pSel->InfoCaption, cgo, iX, iY); } } } void C4Menu::DrawFrame(C4Surface * sfcSurface, int32_t iX, int32_t iY, int32_t iWdt, int32_t iHgt) { pDraw->DrawFrameDw(sfcSurface, iX+1, iY+1, iX+iWdt-1,iY+iHgt-1, C4RGB(0x44, 0, 0)); } void C4Menu::SetAlignment(int32_t iAlignment) { Alignment = iAlignment; } void C4Menu::SetPermanent(bool fPermanent) { Permanent = fPermanent; } bool C4Menu::RefillInternal() { // Reset flag NeedRefill=false; // do the refill in frozen window (no scrolling update) int32_t iLastItemCount=ItemCount; bool fRefilled = false; pClientWindow->Freeze(); bool fSuccess = DoRefillInternal(fRefilled); pClientWindow->UnFreeze(); UpdateElementPositions(); if (!fSuccess) return false; // menu contents may have changed: Adjust menu size and selection if (fRefilled) { // Adjust selection AdjustSelection(); // Item count increased: resize if (ItemCount > iLastItemCount) LocationSet = false; // Item count decreased: resize if we are a context menu if ((ItemCount < iLastItemCount) && IsContextMenu()) LocationSet = false; } // Success return true; } void C4Menu::ClearItems() { C4MenuItem *pItem; while ((pItem = GetItem(0))) delete pItem; ItemCount=0; UpdateScrollBar(); } void C4Menu::Execute() { if (!IsActive()) return; // Refill (timer or flag) if (!Game.iTick35 || NeedRefill) if (!RefillInternal()) Close(false); } bool C4Menu::Refill() { if (!IsActive()) return false; // Refill (close if failure) if (!RefillInternal()) { Close(false); return false; } // Success return true; } void C4Menu::AdjustSelection() { // selection valid? C4MenuItem *pSelection = GetItem(Selection); int iSel = Selection; if (!pSelection) { // set to new first valid selection: Downwards first iSel = Selection; while (--iSel>=0) if ((pSelection = GetItem(iSel))) break; // no success: upwards then if (iSel<0) for (iSel=Selection+1; (pSelection = GetItem(iSel)); ++iSel) break; } // set it then if (!pSelection) SetSelection(-1, Selection >= 0, false); else SetSelection(iSel, iSel != Selection, true); } bool C4Menu::SetLocation(int32_t iX, int32_t iY) { // just set position... SetPos(iX, iY); return true; } C4Viewport *C4Menu::GetViewport() { // ask all viewports for (C4Viewport *pVP = ::Viewports.GetFirstViewport(); pVP; pVP = pVP->GetNext()) if (pVP->IsViewportMenu(this)) return pVP; // none matching return NULL; } void C4Menu::UpdateElementPositions() { // only if already shown and made visible by first drawing // this will postpone the call of menu initialization until it's really needed if (!IsVisible() || !pClientWindow) return; // reposition client scrolling window pClientWindow->SetBounds(GetContainedClientRect()); // re-stack all list items C4MenuItem *pCurr = static_cast(pClientWindow->GetFirst()); // recheck list items int32_t iMaxDlgOptionHeight = -1; int32_t iIndex = 0; C4Rect rcNewBounds(0,0,ItemWidth,ItemHeight); C4MenuItem *pFirstStack = pCurr, *pNext = pFirstStack; while ((pCurr = pNext)) { pNext = static_cast(pCurr->GetNext()); rcNewBounds.x = (iIndex % std::max(Columns, 1)) * ItemWidth; rcNewBounds.y = (iIndex / std::max(Columns, 1)) * ItemHeight; if (pCurr->GetBounds() != rcNewBounds) { pCurr->GetBounds() = rcNewBounds; pCurr->UpdateOwnPos(); } ++iIndex; } // update scrolling pClientWindow->SetClientHeight(rcNewBounds.y + rcNewBounds.Hgt); // re-set caption C4MenuItem *pSel = GetSelectedItem(); const char *szCapt; if (pSel && Style == C4MN_Style_Normal) szCapt = pSel->Caption; else szCapt = Caption; SetTitle((*szCapt) ? szCapt : " ", HasMouse()); } void C4Menu::UpdateOwnPos() { // client rect and stuff typedef C4GUI::Dialog ParentClass; ParentClass::UpdateOwnPos(); UpdateElementPositions(); } void C4Menu::UserSelectItem(int32_t Player, C4MenuItem *pItem) { // not if user con't control anything if (IsReadOnly()) return; if (!pItem) return; // queue or direct selection OnUserSelectItem(Player, pItem->iIndex); } void C4Menu::UserEnter(int32_t Player, C4MenuItem *pItem) { // not if user con't control anything if (IsReadOnly()) return; if (!pItem) return; // queue or direct enter OnUserEnter(Player, pItem->iIndex); } void C4Menu::UserClose(bool fOK) { // not if user con't control anything if (IsReadOnly()) return; // queue or direct enter OnUserClose(); } void C4Menu::SetCloseCommand(const char *strCommand) { CloseCommand.Copy(strCommand); } bool C4Menu::HasMouse() { int32_t iPlayer = GetControllingPlayer(); if (iPlayer == NO_OWNER) return true; // free view dialog also has the mouse C4Player *pPlr = ::Players.Get(iPlayer); if (pPlr && pPlr->MouseControl) return true; return false; } #ifdef _DEBUG void C4Menu::AssertSurfaceNotUsed(C4Surface *sfc) { C4MenuItem *pItem; if (!sfc) return; assert(sfc != Symbol.Surface); for (int32_t i=0; (pItem = GetItem(i)); ++i) assert(pItem->Symbol.Surface != sfc) ; } #endif