/* * 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 // container for a dynamic number of vertically stacked controls #include "C4Include.h" #include "gui/C4Gui.h" #include "graphics/C4Draw.h" #include "gui/C4MouseControl.h" namespace C4GUI { // ---------------------------------------------------- // ListBox ListBox::ListBox(const C4Rect &rtBounds, int32_t iMultiColItemWidth) : Control(rtBounds), iMultiColItemWidth(iMultiColItemWidth), iColCount(1) , pSelectedItem(nullptr), pSelectionChangeHandler(nullptr), pSelectionDblClickHandler(nullptr), fDrawBackground(true), fDrawBorder(false), fSelectionDisabled(false) { // calc client rect UpdateOwnPos(); // create content scroll window pClientWindow = new ScrollWindow(this); // calc column count UpdateColumnCount(); // create key bindings pKeyContext = new C4KeyBinding(C4KeyCodeEx(K_MENU), "GUIListBoxContext", KEYSCOPE_Gui, new ControlKeyCB(*this, &ListBox::KeyContext), C4CustomKey::PRIO_Ctrl); C4CustomKey::CodeList keys; keys.emplace_back(K_UP); if (Config.Controls.GamepadGuiControl) ControllerKeys::Up(keys); pKeyUp = new C4KeyBinding(keys, "GUIListBoxUp", KEYSCOPE_Gui, new ControlKeyCB(*this, &ListBox::KeyUp), C4CustomKey::PRIO_Ctrl); keys.clear(); keys.emplace_back(K_DOWN); if (Config.Controls.GamepadGuiControl) ControllerKeys::Down(keys); pKeyDown = new C4KeyBinding(keys, "GUIListBoxDown", KEYSCOPE_Gui, new ControlKeyCB(*this, &ListBox::KeyDown), C4CustomKey::PRIO_Ctrl); keys.clear(); keys.emplace_back(K_LEFT); if (Config.Controls.GamepadGuiControl) ControllerKeys::Left(keys); pKeyLeft = new C4KeyBinding(keys, "GUIListBoxLeft", KEYSCOPE_Gui, new ControlKeyCB(*this, &ListBox::KeyLeft), C4CustomKey::PRIO_Ctrl); keys.clear(); keys.emplace_back(K_RIGHT); if (Config.Controls.GamepadGuiControl) ControllerKeys::Right(keys); pKeyRight = new C4KeyBinding(keys, "GUIListBoxRight", KEYSCOPE_Gui, new ControlKeyCB(*this, &ListBox::KeyRight), C4CustomKey::PRIO_Ctrl); pKeyPageUp = new C4KeyBinding(C4KeyCodeEx(K_PAGEUP), "GUIListBoxPageUp", KEYSCOPE_Gui, new ControlKeyCB(*this, &ListBox::KeyPageUp), C4CustomKey::PRIO_Ctrl); pKeyPageDown = new C4KeyBinding(C4KeyCodeEx(K_PAGEDOWN), "GUIListBoxPageDown", KEYSCOPE_Gui, new ControlKeyCB(*this, &ListBox::KeyPageDown), C4CustomKey::PRIO_Ctrl); pKeyHome = new C4KeyBinding(C4KeyCodeEx(K_HOME), "GUIListBoxHome", KEYSCOPE_Gui, new ControlKeyCB(*this, &ListBox::KeyHome), C4CustomKey::PRIO_Ctrl); pKeyEnd = new C4KeyBinding(C4KeyCodeEx(K_END), "GUIListBoxEnd", KEYSCOPE_Gui, new ControlKeyCB(*this, &ListBox::KeyEnd), C4CustomKey::PRIO_Ctrl); // "activate" current item keys.clear(); keys.emplace_back(K_RETURN); keys.emplace_back(K_RETURN, KEYS_Alt); if (Config.Controls.GamepadGuiControl) { ControllerKeys::Ok(keys); } pKeyActivate = new C4KeyBinding(keys, "GUIListActivate", KEYSCOPE_Gui, new ControlKeyCB(*this, &ListBox::KeyActivate), C4CustomKey::PRIO_Ctrl); } ListBox::~ListBox() { delete pKeyActivate; delete pKeyEnd; delete pKeyHome; delete pKeyPageDown; delete pKeyPageUp; delete pKeyRight; delete pKeyLeft; delete pKeyDown; delete pKeyUp; delete pKeyContext; if (pSelectionDblClickHandler) pSelectionDblClickHandler->DeRef(); if (pSelectionChangeHandler) pSelectionChangeHandler->DeRef(); } void ListBox::DrawElement(C4TargetFacet &cgo) { if (fDrawBackground) pDraw->DrawBoxDw(cgo.Surface, cgo.TargetX+rcBounds.x, cgo.TargetY+rcBounds.y, cgo.TargetX+rcBounds.x+rcBounds.Wdt-1, cgo.TargetY+rcBounds.y+rcBounds.Hgt-1, 0x7f000000); if (fDrawBorder) Draw3DFrame(cgo); // listbox bg: mark selected item if (!pClientWindow) return; if (pSelectedItem) { C4Rect rcSelArea = pSelectedItem->GetBounds(); rcSelArea.x += GetClientRect().x; rcSelArea.y += GetClientRect().y + pClientWindow->GetClientRect().y; // clip if (rcSelArea.y < GetClientRect().y) { rcSelArea.Hgt -= GetClientRect().y - rcSelArea.y; rcSelArea.y = GetClientRect().y; } rcSelArea.Hgt = std::min(rcSelArea.Hgt, GetClientRect().y + GetClientRect().Hgt - rcSelArea.y); // draw if (rcSelArea.Hgt>=0) 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, HasDrawFocus() ? C4GUI_ListBoxSelColor : C4GUI_ListBoxInactSelColor); } // draw delimeter bars Element *pCurr = pClientWindow->GetFirst(); if (!pCurr) return; while ((pCurr = pCurr->GetNext())) if (pCurr->GetListItemTopSpacingBar()) { int32_t iYSpace = pCurr->GetListItemTopSpacing(); int32_t iY = pCurr->GetBounds().y + GetClientRect().y + pClientWindow->GetClientRect().y - iYSpace/2; int32_t iX0 = pCurr->GetBounds().x + GetClientRect().x + C4GUI_ListBoxBarIndent; int32_t iX1 = iX0 + pClientWindow->GetClientRect().Wdt - 2*C4GUI_ListBoxBarIndent; // clip if (iY < GetClientRect().y || iY >= GetClientRect().y+GetClientRect().Hgt) continue; // draw pDraw->DrawLineDw(cgo.Surface, (float)(iX0+cgo.TargetX), (float)(iY+cgo.TargetY), (float)(iX1+cgo.TargetX), (float)(iY+cgo.TargetY), C4GUI_ListBoxBarColor); } } void ListBox::MouseInput(CMouse &rMouse, int32_t iButton, int32_t iX, int32_t iY, DWORD dwKeyParam) { // inherited Control::MouseInput(rMouse, iButton, iX, iY, dwKeyParam); // safety if (pClientWindow) { // check list area bounds if (pClientWindow->GetBounds().Contains(iX, iY)) // left btn down: select item (regardless of key states) if (iButton == C4MC_Button_LeftDown || iButton == C4MC_Button_LeftDouble) { // reset selection Element *pPrevSelectedItem = pSelectedItem; pSelectedItem = nullptr; // get client component the mouse is over iX -= GetMarginLeft(); iY -= GetMarginTop(); iY += pClientWindow->GetScrollY(); for (Element *pCurr = pClientWindow->GetFirst(); pCurr; pCurr = pCurr->GetNext()) if (pCurr->GetBounds().Contains(iX, iY)) pSelectedItem = pCurr; // selection change sound if (pSelectedItem != pPrevSelectedItem) SelectionChanged(true); // item double-clicked? Callback if (iButton == C4MC_Button_LeftDouble && pSelectedItem) if (pSelectionDblClickHandler) pSelectionDblClickHandler->DoCall(pSelectedItem); } } } void ListBox::UpdateColumnCount() { if (iMultiColItemWidth && pClientWindow) { // multicoloumn-listbox iColCount = std::max(pClientWindow->GetClientRect().Wdt / iMultiColItemWidth, 1); } else { // regular 1-col-listbox iColCount = 1; } } int32_t ListBox::ContractToElementHeight() { if (!pClientWindow) return 0; // calc superfluous bottom space int32_t iExtraSpace = pClientWindow->GetBounds().Hgt - pClientWindow->GetClientRect().Hgt; if (iExtraSpace <= 0) return 0; // contract by it C4Rect rcNewBounds = GetBounds(); rcNewBounds.Hgt -= iExtraSpace; SetBounds(rcNewBounds); return iExtraSpace; } void ListBox::OnGetFocus(bool fByMouse) { // inherited (tooltip) Control::OnGetFocus(fByMouse); // select list item if none is selected (only for keyboard; mouse will select with left-click anyway) if (!pSelectedItem && pClientWindow && !fByMouse) { pSelectedItem = pClientWindow->GetFirstContained(); SelectionChanged(false); } } bool ListBox::KeyContext() { // key: context menu if (pSelectedItem && pSelectedItem->DoContext()) return true; return false; } bool ListBox::KeyUp() { // key: selection up Element *pPrevSelectedItem = pSelectedItem; if (!pSelectedItem) // select last pSelectedItem = pClientWindow->GetLastContained(); else { // select prev row int32_t cnt = iColCount; while (pSelectedItem && cnt--) pSelectedItem = pSelectedItem->GetPrev(); if (!pSelectedItem) pSelectedItem = pPrevSelectedItem; // was in start row } // selection might have changed if (pSelectedItem != pPrevSelectedItem) SelectionChanged(true); return true; } bool ListBox::KeyDown() { // key: selection down Element *pPrevSelectedItem = pSelectedItem; if (!pSelectedItem) // select first pSelectedItem = pClientWindow->GetFirstContained(); else { // select next row int32_t cnt = iColCount; while (pSelectedItem && cnt--) pSelectedItem = pSelectedItem->GetNext(); if (!pSelectedItem) pSelectedItem = pPrevSelectedItem; // was in end row } // selection might have changed if (pSelectedItem != pPrevSelectedItem) SelectionChanged(true); return true; } bool ListBox::KeyLeft() { // key: Selection left // only in multi-col-listboxes if (!IsMultiColumn()) return false; Element *pPrevSelectedItem = pSelectedItem; if (!pSelectedItem) // select last pSelectedItem = pClientWindow->GetLastContained(); else { // select prev if (pSelectedItem->GetPrev()) pSelectedItem = pSelectedItem->GetPrev(); } // selection might have changed if (pSelectedItem != pPrevSelectedItem) SelectionChanged(true); return true; } bool ListBox::KeyRight() { // key: Selection right // only in multi-col-listboxes if (!IsMultiColumn()) return false; Element *pPrevSelectedItem = pSelectedItem; if (!pSelectedItem) // select first pSelectedItem = pClientWindow->GetFirstContained(); else { // select next if (pSelectedItem->GetNext()) pSelectedItem = pSelectedItem->GetNext(); } // selection might have changed if (pSelectedItem != pPrevSelectedItem) SelectionChanged(true); return true; } bool ListBox::KeyPageDown() { // key: selection one page down // start from first item or selected Element *pNextSelectedItem = pSelectedItem ? pSelectedItem : pClientWindow->GetFirstContained(), *pNext; if (!pNextSelectedItem) return false; if ((pNext = pNextSelectedItem->GetNext())) { pNextSelectedItem = pNext; // if this is not the last, visible item in the list: go down until item is no longer fully in view if (pClientWindow->IsRangeInView(pNextSelectedItem->GetBounds().y, pNextSelectedItem->GetBounds().Hgt)) { while ((pNext = pNextSelectedItem->GetNext())) if (pClientWindow->IsRangeInView(pNext->GetBounds().y, pNext->GetBounds().Hgt)) pNextSelectedItem = pNext; else break; } else { // selected item was last visible: Just scroll one page down and select last visible pClientWindow->ScrollPages(+1); pNextSelectedItem = pClientWindow->GetLastContained(); while (!pClientWindow->IsRangeInView(pNextSelectedItem->GetBounds().y, pNextSelectedItem->GetBounds().Hgt)) if ((pNext = pNextSelectedItem->GetPrev())) pNextSelectedItem = pNext; else break; } } // selection might have changed if (pSelectedItem != pNextSelectedItem) { pSelectedItem = pNextSelectedItem; SelectionChanged(true); } return true; } bool ListBox::KeyPageUp() { // key: selection one page up // start from last item or selected Element *pNextSelectedItem = pSelectedItem ? pSelectedItem : pClientWindow->GetLastContained(), *pNext; if (!pNextSelectedItem) return false; if ((pNext = pNextSelectedItem->GetPrev())) { pNextSelectedItem = pNext; // if this is not the first, visible item in the list: go up until item is no longer fully in view if (pClientWindow->IsRangeInView(pNextSelectedItem->GetBounds().y, pNextSelectedItem->GetBounds().Hgt)) { while ((pNext = pNextSelectedItem->GetPrev())) if (pClientWindow->IsRangeInView(pNext->GetBounds().y, pNext->GetBounds().Hgt)) pNextSelectedItem = pNext; else break; } else { // selected item was last visible: Just scroll one page up and select first visible pClientWindow->ScrollPages(-1); pNextSelectedItem = pClientWindow->GetFirstContained(); while (!pClientWindow->IsRangeInView(pNextSelectedItem->GetBounds().y, pNextSelectedItem->GetBounds().Hgt)) if ((pNext = pNextSelectedItem->GetNext())) pNextSelectedItem = pNext; else break; } } // selection might have changed if (pSelectedItem != pNextSelectedItem) { pSelectedItem = pNextSelectedItem; SelectionChanged(true); } return true; } bool ListBox::KeyHome() { // key: selection to first item Element *pPrevSelectedItem = pSelectedItem; pSelectedItem = pClientWindow->GetFirstContained(); // selection might have changed if (pSelectedItem != pPrevSelectedItem) SelectionChanged(true); return true; } bool ListBox::KeyEnd() { // key: selection to last item Element *pPrevSelectedItem = pSelectedItem; pSelectedItem = pClientWindow->GetLastContained(); // selection might have changed if (pSelectedItem != pPrevSelectedItem) SelectionChanged(true); return true; } bool ListBox::KeyActivate() { // process as doubleclick if (pSelectedItem && pSelectionDblClickHandler) { pSelectionDblClickHandler->DoCall(pSelectedItem); return true; } return false; } void ListBox::ScrollItemInView(Element *pItem) { // safety if (!pItem) return; // scroll covered range into view pClientWindow->ScrollRangeInView(pItem->GetBounds().y, pItem->GetBounds().Hgt); } void ListBox::UpdateElementPositions() { // safety if (!pClientWindow) return; // first item at zero offset Element *pCurr = pClientWindow->GetFirst(); int iOverallHgt; if (pCurr) { if (!iMultiColItemWidth) { // Single column box: All stacked vertically if (pCurr->GetBounds().y) { pCurr->GetBounds().y = 0; pCurr->UpdateOwnPos(); } if(pCurr->fVisible) iOverallHgt = pCurr->GetBounds().Hgt; else iOverallHgt = 0; // others stacked under it while ((pCurr = pCurr->GetNext())) { if(!pCurr->fVisible) continue; //Do not reserve space for hidden elements 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(); } } } else { // Multi column box: Keep element size; reposition horizontally+vertically int32_t y=0, iLineHgt=0, col=0; for (; pCurr; pCurr=pCurr->GetNext()) { const C4Rect &rcCurrBounds = pCurr->GetBounds(); iLineHgt = std::max(rcCurrBounds.Hgt, iLineHgt); int32_t x = col * iMultiColItemWidth; if (rcCurrBounds.x != x || rcCurrBounds.y != y || rcCurrBounds.Wdt != iMultiColItemWidth) pCurr->SetBounds(C4Rect(x,y,iMultiColItemWidth,rcCurrBounds.Hgt)); if (++col >= iColCount) { col = 0; y += iLineHgt; } } iOverallHgt = y + iLineHgt; } } else iOverallHgt = 0; // update scrolling pClientWindow->SetClientHeight(iOverallHgt); } void ListBox::UpdateElementPosition(Element *pOfElement, int32_t iIndent) { // resize it C4Rect &rcChildBounds = pOfElement->GetBounds(); rcChildBounds.x = iIndent; rcChildBounds.Wdt = GetItemWidth() - iIndent ; pOfElement->UpdateOwnPos(); // re-stack elements UpdateElementPositions(); } void ListBox::RemoveElement(Element *pChild) { // inherited Control::RemoveElement(pChild); // clear selection var if (pChild == pSelectedItem) { pSelectedItem = nullptr; SelectionChanged(false); } // position update in AfterElementRemoval } bool ListBox::AddElement(Element *pChild, int32_t iIndent) { // fail if no client window is present if (!pClientWindow) return false; // add to scroll window pClientWindow->AddElement(pChild); // resize to horizontal list extents C4Rect &rcChildBounds = pChild->GetBounds(); rcChildBounds.x = iIndent; rcChildBounds.Wdt = GetItemWidth() - iIndent ; // reposition to end of list if (pChild->GetPrev()) { if (iMultiColItemWidth) { rcChildBounds.y = pChild->GetPrev()->GetBounds().y; int32_t col = pChild->GetPrev()->GetBounds().x / iMultiColItemWidth + 1; if (col >= iColCount) { col = 0; int32_t cnt = iColCount; int32_t iPrevLineHgt = 0; Element *pPrevChild = pChild->GetPrev(); while (cnt-- && pPrevChild) { iPrevLineHgt = std::max(iPrevLineHgt, pPrevChild->GetBounds().Hgt); pPrevChild = pPrevChild->GetPrev(); } rcChildBounds.y += iPrevLineHgt; } rcChildBounds.x = col * iMultiColItemWidth; } else { rcChildBounds.y = pChild->GetPrev()->GetBounds().y + pChild->GetPrev()->GetBounds().Hgt + pChild->GetListItemTopSpacing(); } } else rcChildBounds.y = 0; pChild->UpdateOwnPos(); // update scrolling pClientWindow->SetClientHeight(rcChildBounds.y+rcChildBounds.Hgt); // success return true; } bool ListBox::InsertElement(Element *pChild, Element *pInsertBefore, int32_t iIndent) { // fail if no client window is present if (!pClientWindow) return false; // add to scroll window pClientWindow->InsertElement(pChild, pInsertBefore); // resize to horizontal list extents C4Rect &rcChildBounds = pChild->GetBounds(); rcChildBounds.x = iIndent; rcChildBounds.Wdt = GetItemWidth() - iIndent ; pChild->UpdateOwnPos(); // update all element positions (and scrolling) UpdateElementPositions(); // done, success return true; } void ListBox::ElementSizeChanged(Element *pOfElement) { // inherited if (pOfElement->GetParent() == this) { Control::ElementSizeChanged(pOfElement); // update col count if list element container was resized UpdateColumnCount(); } // update positions of all list items UpdateElementPositions(); } void ListBox::ElementPosChanged(Element *pOfElement) { // inherited if (pOfElement->GetParent() == this) Control::ElementSizeChanged(pOfElement); // update positions of all list items UpdateElementPositions(); } void ListBox::SelectionChanged(bool fByUser) { // selections disabled? if (fSelectionDisabled) { pSelectedItem = nullptr; return; } // any selection? if (pSelectedItem) { // effect if (fByUser) GUISound("UI::Select"); } // callback (caution: May do periluous things...) if (pSelectionChangeHandler) pSelectionChangeHandler->DoCall(pSelectedItem); // let's hope it wasn't perilous enough to delete this, // because scrolling the item into view must be done AFTER the callback, as the callback might resize if (pSelectedItem) ScrollItemInView(pSelectedItem); } void ListBox::SelectEntry(Element *pNewSel, bool fByUser) { assert(!pNewSel || pNewSel->GetParent() == pClientWindow); if (pSelectedItem == pNewSel) return; pSelectedItem = pNewSel; SelectionChanged(fByUser); } bool ListBox::CharIn(const char * c) { // Jump to first/next entry beginning with typed letter Element *pSel = GetSelectedItem(); Element *pStartCheck = pSel; if (pSel) pSel = pSel->GetNext(); if (!pSel) { pSel = GetFirst(); if (!pSel) return false; } while (pSel != pStartCheck && !pSel->CheckNameHotkey(c)) if (!(pSel = pSel->GetNext())) if (pStartCheck) // list end reached while another entry had been selected before: Re-check start of list pSel = GetFirst(); // ok, change selection - might do nothing if list was cycled, which is OK if (pSel) { SelectEntry(pSel, true); return true; } return Control::CharIn(c); } class SortCompareElements { void *par; ListBox::SortFunction SortFunc; public: SortCompareElements(ListBox::SortFunction SortFunc, void *par) : par(par), SortFunc(SortFunc) {} int operator()(const Element *pEl1, const Element *pEl2) { return (*SortFunc)(pEl1, pEl2, par)>0; } }; void ListBox::SortElements(SortFunction SortFunc, void *par) { // sort list items: // create an array of all list items, sort it, and reorder them afterwards if (!pClientWindow) return; int32_t iElemCount = pClientWindow->GetElementCount(); if (iElemCount <= 1) return; Element **ppElements = new Element *[iElemCount]; try { int32_t i=0; for (Element *pEl = pClientWindow->GetFirst(); pEl; pEl = pEl->GetNext()) ppElements[i++] = pEl; std::sort(ppElements, ppElements+iElemCount, SortCompareElements(SortFunc, par)); for (i=0; iReaddElement(ppElements[i]); } catch (...) { delete [] ppElements; throw; } delete [] ppElements; UpdateElementPositions(); } } // end of namespace