diff options
Diffstat (limited to 'xbmc/guilib/GUIControlGroupList.cpp')
-rw-r--r-- | xbmc/guilib/GUIControlGroupList.cpp | 601 |
1 files changed, 601 insertions, 0 deletions
diff --git a/xbmc/guilib/GUIControlGroupList.cpp b/xbmc/guilib/GUIControlGroupList.cpp new file mode 100644 index 0000000..5252d67 --- /dev/null +++ b/xbmc/guilib/GUIControlGroupList.cpp @@ -0,0 +1,601 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIControlGroupList.h" + +#include "GUIAction.h" +#include "GUIControlProfiler.h" +#include "GUIFont.h" // for XBFONT_* definitions +#include "GUIMessage.h" +#include "guilib/guiinfo/GUIInfoLabels.h" +#include "input/Key.h" +#include "utils/StringUtils.h" + +CGUIControlGroupList::CGUIControlGroupList(int parentID, int controlID, float posX, float posY, float width, float height, float itemGap, int pageControl, ORIENTATION orientation, bool useControlPositions, uint32_t alignment, const CScroller& scroller) +: CGUIControlGroup(parentID, controlID, posX, posY, width, height) +, m_scroller(scroller) +{ + m_itemGap = itemGap; + m_pageControl = pageControl; + m_focusedPosition = 0; + m_totalSize = 0; + m_orientation = orientation; + m_alignment = alignment; + m_lastScrollerValue = -1; + m_useControlPositions = useControlPositions; + ControlType = GUICONTROL_GROUPLIST; + m_minSize = 0; +} + +CGUIControlGroupList::~CGUIControlGroupList(void) = default; + +void CGUIControlGroupList::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) +{ + if (m_scroller.Update(currentTime)) + MarkDirtyRegion(); + + // first we update visibility of all our items, to ensure our size and + // alignment computations are correct. + for (iControls it = m_children.begin(); it != m_children.end(); ++it) + { + CGUIControl *control = *it; + GUIPROFILER_VISIBILITY_BEGIN(control); + control->UpdateVisibility(nullptr); + GUIPROFILER_VISIBILITY_END(control); + } + + // visibility status of some of the list items may have changed. Thus, the group list size + // may now be different and the scroller needs to be updated + int previousTotalSize = m_totalSize; + ValidateOffset(); // m_totalSize is updated here + bool sizeChanged = previousTotalSize != m_totalSize; + + if (m_pageControl && (m_lastScrollerValue != m_scroller.GetValue() || sizeChanged)) + { + CGUIMessage message(GUI_MSG_LABEL_RESET, GetParentID(), m_pageControl, (int)Size(), (int)m_totalSize); + SendWindowMessage(message); + CGUIMessage message2(GUI_MSG_ITEM_SELECT, GetParentID(), m_pageControl, (int)m_scroller.GetValue()); + SendWindowMessage(message2); + m_lastScrollerValue = static_cast<int>(m_scroller.GetValue()); + } + // we run through the controls, rendering as we go + int index = 0; + float pos = GetAlignOffset(); + for (iControls it = m_children.begin(); it != m_children.end(); ++it) + { + // note we render all controls, even if they're offscreen, as then they'll be updated + // with respect to animations + CGUIControl *control = *it; + if (m_orientation == VERTICAL) + CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX, m_posY + pos - m_scroller.GetValue()); + else + CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX + pos - m_scroller.GetValue(), m_posY); + control->DoProcess(currentTime, dirtyregions); + + if (control->IsVisible()) + { + if (IsControlOnScreen(pos, control)) + { + if (control->HasFocus()) + m_focusedPosition = index; + index++; + } + + pos += Size(control) + m_itemGap; + } + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin(); + } + CGUIControl::Process(currentTime, dirtyregions); +} + +void CGUIControlGroupList::Render() +{ + // we run through the controls, rendering as we go + bool render(CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_posX, m_posY, m_width, m_height)); + float pos = GetAlignOffset(); + float focusedPos = 0; + CGUIControl *focusedControl = NULL; + for (iControls it = m_children.begin(); it != m_children.end(); ++it) + { + // note we render all controls, even if they're offscreen, as then they'll be updated + // with respect to animations + CGUIControl *control = *it; + if (m_renderFocusedLast && control->HasFocus()) + { + focusedControl = control; + focusedPos = pos; + } + else + { + if (m_orientation == VERTICAL) + CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX, m_posY + pos - m_scroller.GetValue()); + else + CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX + pos - m_scroller.GetValue(), m_posY); + control->DoRender(); + } + if (control->IsVisible()) + pos += Size(control) + m_itemGap; + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin(); + } + if (focusedControl) + { + if (m_orientation == VERTICAL) + CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX, m_posY + focusedPos - m_scroller.GetValue()); + else + CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX + focusedPos - m_scroller.GetValue(), m_posY); + focusedControl->DoRender(); + } + if (render) CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion(); + CGUIControl::Render(); +} + +bool CGUIControlGroupList::OnMessage(CGUIMessage& message) +{ + switch (message.GetMessage() ) + { + case GUI_MSG_FOCUSED: + { // a control has been focused + // scroll if we need to and update our page control + ValidateOffset(); + float offset = 0; + for (iControls it = m_children.begin(); it != m_children.end(); ++it) + { + CGUIControl *control = *it; + if (!control->IsVisible()) + continue; + if (control->GetControl(message.GetControlId())) + { + // find out whether this is the first or last control + if (IsFirstFocusableControl(control)) + ScrollTo(0); + else if (IsLastFocusableControl(control)) + ScrollTo(m_totalSize - Size()); + else if (offset < m_scroller.GetValue()) + ScrollTo(offset); + else if (offset + Size(control) > m_scroller.GetValue() + Size()) + ScrollTo(offset + Size(control) - Size()); + break; + } + offset += Size(control) + m_itemGap; + } + } + break; + case GUI_MSG_SETFOCUS: + { + // we've been asked to focus. We focus the last control if it's on this page, + // else we'll focus the first focusable control from our offset (after verifying it) + ValidateOffset(); + // now check the focusControl's offset + float offset = 0; + for (iControls it = m_children.begin(); it != m_children.end(); ++it) + { + CGUIControl *control = *it; + if (!control->IsVisible()) + continue; + if (control->GetControl(m_focusedControl)) + { + if (IsControlOnScreen(offset, control)) + return CGUIControlGroup::OnMessage(message); + break; + } + offset += Size(control) + m_itemGap; + } + // find the first control on this page + offset = 0; + for (iControls it = m_children.begin(); it != m_children.end(); ++it) + { + CGUIControl *control = *it; + if (!control->IsVisible()) + continue; + if (control->CanFocus() && IsControlOnScreen(offset, control)) + { + m_focusedControl = control->GetID(); + break; + } + offset += Size(control) + m_itemGap; + } + } + break; + case GUI_MSG_PAGE_CHANGE: + { + if (message.GetSenderId() == m_pageControl) + { // it's from our page control + ScrollTo((float)message.GetParam1()); + return true; + } + } + break; + } + return CGUIControlGroup::OnMessage(message); +} + +void CGUIControlGroupList::ValidateOffset() +{ + // calculate item gap. this needs to be done + // before fetching the total size + CalculateItemGap(); + // calculate how many items we have on this page + m_totalSize = GetTotalSize(); + // check our m_offset range + if (m_scroller.GetValue() > m_totalSize - Size()) + m_scroller.SetValue(m_totalSize - Size()); + if (m_scroller.GetValue() < 0) m_scroller.SetValue(0); +} + +void CGUIControlGroupList::AddControl(CGUIControl *control, int position /*= -1*/) +{ + // NOTE: We override control navigation here, but we don't override the <onleft> etc. builtins + // if specified. + if (position < 0 || position > (int)m_children.size()) // add at the end + position = (int)m_children.size(); + + if (control) + { // set the navigation of items so that they form a list + CGUIAction beforeAction = GetAction((m_orientation == VERTICAL) ? ACTION_MOVE_UP : ACTION_MOVE_LEFT); + CGUIAction afterAction = GetAction((m_orientation == VERTICAL) ? ACTION_MOVE_DOWN : ACTION_MOVE_RIGHT); + if (m_children.size()) + { + // we're inserting at the given position, so grab the items above and below and alter + // their navigation accordingly + CGUIControl *before = NULL; + CGUIControl *after = NULL; + if (position == 0) + { // inserting at the beginning + after = m_children[0]; + if (!afterAction.HasActionsMeetingCondition() || afterAction.GetNavigation() == GetID()) // we're wrapping around bottom->top, so we have to update the last item + before = m_children[m_children.size() - 1]; + if (!beforeAction.HasActionsMeetingCondition() || beforeAction.GetNavigation() == GetID()) // we're wrapping around top->bottom + beforeAction = CGUIAction(m_children[m_children.size() - 1]->GetID()); + afterAction = CGUIAction(after->GetID()); + } + else if (position == (int)m_children.size()) + { // inserting at the end + before = m_children[m_children.size() - 1]; + if (!beforeAction.HasActionsMeetingCondition() || beforeAction.GetNavigation() == GetID()) // we're wrapping around top->bottom, so we have to update the first item + after = m_children[0]; + if (!afterAction.HasActionsMeetingCondition() || afterAction.GetNavigation() == GetID()) // we're wrapping around bottom->top + afterAction = CGUIAction(m_children[0]->GetID()); + beforeAction = CGUIAction(before->GetID()); + } + else + { // inserting somewhere in the middle + before = m_children[position - 1]; + after = m_children[position]; + beforeAction = CGUIAction(before->GetID()); + afterAction = CGUIAction(after->GetID()); + } + if (m_orientation == VERTICAL) + { + if (before) // update the DOWN action to point to us + before->SetAction(ACTION_MOVE_DOWN, CGUIAction(control->GetID())); + if (after) // update the UP action to point to us + after->SetAction(ACTION_MOVE_UP, CGUIAction(control->GetID())); + } + else + { + if (before) // update the RIGHT action to point to us + before->SetAction(ACTION_MOVE_RIGHT, CGUIAction(control->GetID())); + if (after) // update the LEFT action to point to us + after->SetAction(ACTION_MOVE_LEFT, CGUIAction(control->GetID())); + } + } + // now the control's nav + // set navigation path on orientation axis + // and try to apply other nav actions from grouplist + // don't override them if child have already defined actions + if (m_orientation == VERTICAL) + { + control->SetAction(ACTION_MOVE_UP, beforeAction); + control->SetAction(ACTION_MOVE_DOWN, afterAction); + control->SetAction(ACTION_MOVE_LEFT, GetAction(ACTION_MOVE_LEFT), false); + control->SetAction(ACTION_MOVE_RIGHT, GetAction(ACTION_MOVE_RIGHT), false); + } + else + { + control->SetAction(ACTION_MOVE_LEFT, beforeAction); + control->SetAction(ACTION_MOVE_RIGHT, afterAction); + control->SetAction(ACTION_MOVE_UP, GetAction(ACTION_MOVE_UP), false); + control->SetAction(ACTION_MOVE_DOWN, GetAction(ACTION_MOVE_DOWN), false); + } + control->SetAction(ACTION_NAV_BACK, GetAction(ACTION_NAV_BACK), false); + + if (!m_useControlPositions) + control->SetPosition(0,0); + CGUIControlGroup::AddControl(control, position); + m_totalSize = GetTotalSize(); + } +} + +void CGUIControlGroupList::ClearAll() +{ + m_totalSize = 0; + CGUIControlGroup::ClearAll(); + m_scroller.SetValue(0); +} + +#define CLAMP(x, low, high) (((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x))) + +float CGUIControlGroupList::GetWidth() const +{ + if (m_orientation == HORIZONTAL) + return CLAMP(m_totalSize, m_minSize, m_width); + return CGUIControlGroup::GetWidth(); +} + +float CGUIControlGroupList::GetHeight() const +{ + if (m_orientation == VERTICAL) + return CLAMP(m_totalSize, m_minSize, m_height); + return CGUIControlGroup::GetHeight(); +} + +void CGUIControlGroupList::SetMinSize(float minWidth, float minHeight) +{ + if (m_orientation == VERTICAL) + m_minSize = minHeight; + else + m_minSize = minWidth; +} + +float CGUIControlGroupList::Size(const CGUIControl *control) const +{ + return (m_orientation == VERTICAL) ? control->GetYPosition() + control->GetHeight() : control->GetXPosition() + control->GetWidth(); +} + +inline float CGUIControlGroupList::Size() const +{ + return (m_orientation == VERTICAL) ? m_height : m_width; +} + +void CGUIControlGroupList::SetInvalid() +{ + CGUIControl::SetInvalid(); + // Force a message to the scrollbar + m_lastScrollerValue = -1; +} + +void CGUIControlGroupList::ScrollTo(float offset) +{ + m_scroller.ScrollTo(offset); + if (m_scroller.IsScrolling()) + SetInvalid(); + MarkDirtyRegion(); +} + +EVENT_RESULT CGUIControlGroupList::SendMouseEvent(const CPoint &point, const CMouseEvent &event) +{ + // transform our position into child coordinates + CPoint childPoint(point); + m_transform.InverseTransformPosition(childPoint.x, childPoint.y); + if (CGUIControl::CanFocus()) + { + float pos = 0; + float alignOffset = GetAlignOffset(); + for (ciControls i = m_children.begin(); i != m_children.end(); ++i) + { + CGUIControl *child = *i; + if (child->IsVisible()) + { + if (IsControlOnScreen(pos, child)) + { // we're on screen + float offsetX = m_orientation == VERTICAL ? m_posX : m_posX + alignOffset + pos - m_scroller.GetValue(); + float offsetY = m_orientation == VERTICAL ? m_posY + alignOffset + pos - m_scroller.GetValue() : m_posY; + EVENT_RESULT ret = child->SendMouseEvent(childPoint - CPoint(offsetX, offsetY), event); + if (ret) + { // we've handled the action, and/or have focused an item + return ret; + } + } + pos += Size(child) + m_itemGap; + } + } + // none of our children want the event, but we may want it. + EVENT_RESULT ret; + if (HitTest(childPoint) && (ret = OnMouseEvent(childPoint, event))) + return ret; + } + m_focusedControl = 0; + return EVENT_RESULT_UNHANDLED; +} + +void CGUIControlGroupList::UnfocusFromPoint(const CPoint &point) +{ + float pos = 0; + CPoint controlCoords(point); + m_transform.InverseTransformPosition(controlCoords.x, controlCoords.y); + float alignOffset = GetAlignOffset(); + for (iControls it = m_children.begin(); it != m_children.end(); ++it) + { + CGUIControl *child = *it; + if (child->IsVisible()) + { + if (IsControlOnScreen(pos, child)) + { // we're on screen + CPoint offset = (m_orientation == VERTICAL) ? CPoint(m_posX, m_posY + alignOffset + pos - m_scroller.GetValue()) : CPoint(m_posX + alignOffset + pos - m_scroller.GetValue(), m_posY); + child->UnfocusFromPoint(controlCoords - offset); + } + pos += Size(child) + m_itemGap; + } + } + CGUIControl::UnfocusFromPoint(point); +} + +bool CGUIControlGroupList::GetCondition(int condition, int data) const +{ + switch (condition) + { + case CONTAINER_HAS_NEXT: + return (m_totalSize >= Size() && m_scroller.GetValue() < m_totalSize - Size()); + case CONTAINER_HAS_PREVIOUS: + return (m_scroller.GetValue() > 0); + case CONTAINER_POSITION: + return (m_focusedPosition == data); + default: + return false; + } +} + +std::string CGUIControlGroupList::GetLabel(int info) const +{ + switch (info) + { + case CONTAINER_CURRENT_ITEM: + return std::to_string(GetSelectedItem()); + case CONTAINER_NUM_ITEMS: + return std::to_string(GetNumItems()); + case CONTAINER_POSITION: + return std::to_string(m_focusedPosition); + default: + break; + } + return ""; +} + +int CGUIControlGroupList::GetNumItems() const +{ + return std::count_if(m_children.begin(), m_children.end(), [&](const CGUIControl *child) { + return (child->IsVisible() && child->CanFocus()); + }); +} + +int CGUIControlGroupList::GetSelectedItem() const +{ + int index = 1; + for (const auto& child : m_children) + { + if (child->IsVisible() && child->CanFocus()) + { + if (child->HasFocus()) + return index; + index++; + } + } + return -1; +} + +bool CGUIControlGroupList::IsControlOnScreen(float pos, const CGUIControl *control) const +{ + return (pos >= m_scroller.GetValue() && pos + Size(control) <= m_scroller.GetValue() + Size()); +} + +bool CGUIControlGroupList::IsFirstFocusableControl(const CGUIControl *control) const +{ + for (ciControls it = m_children.begin(); it != m_children.end(); ++it) + { + CGUIControl *child = *it; + if (child->IsVisible() && child->CanFocus()) + { // found first focusable + return child == control; + } + } + return false; +} + +bool CGUIControlGroupList::IsLastFocusableControl(const CGUIControl *control) const +{ + for (crControls it = m_children.rbegin(); it != m_children.rend(); ++it) + { + CGUIControl *child = *it; + if (child->IsVisible() && child->CanFocus()) + { // found first focusable + return child == control; + } + } + return false; +} + +void CGUIControlGroupList::CalculateItemGap() +{ + if (m_alignment & XBFONT_JUSTIFIED) + { + int itemsCount = 0; + float itemsSize = 0; + for (const auto& child : m_children) + { + if (child->IsVisible()) + { + itemsSize += Size(child); + itemsCount++; + } + } + + if (itemsCount > 0) + m_itemGap = (Size() - itemsSize) / itemsCount; + } +} + +float CGUIControlGroupList::GetAlignOffset() const +{ + if (m_totalSize < Size()) + { + if (m_alignment & XBFONT_RIGHT) + return Size() - m_totalSize; + if (m_alignment & (XBFONT_CENTER_X | XBFONT_JUSTIFIED)) + return (Size() - m_totalSize)*0.5f; + } + return 0.0f; +} + +EVENT_RESULT CGUIControlGroupList::OnMouseEvent(const CPoint &point, const CMouseEvent &event) +{ + if (event.m_id == ACTION_MOUSE_WHEEL_UP || event.m_id == ACTION_MOUSE_WHEEL_DOWN) + { + // find the current control and move to the next or previous + float offset = 0; + for (ciControls it = m_children.begin(); it != m_children.end(); ++it) + { + CGUIControl *control = *it; + if (!control->IsVisible()) continue; + float nextOffset = offset + Size(control) + m_itemGap; + if (event.m_id == ACTION_MOUSE_WHEEL_DOWN && nextOffset > m_scroller.GetValue() && m_scroller.GetValue() < m_totalSize - Size()) // past our current offset + { + ScrollTo(nextOffset); + return EVENT_RESULT_HANDLED; + } + else if (event.m_id == ACTION_MOUSE_WHEEL_UP && nextOffset >= m_scroller.GetValue() && m_scroller.GetValue() > 0) // at least at our current offset + { + ScrollTo(offset); + return EVENT_RESULT_HANDLED; + } + offset = nextOffset; + } + } + else if (event.m_id == ACTION_GESTURE_BEGIN) + { // grab exclusive access + CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, GetID(), GetParentID()); + SendWindowMessage(msg); + return EVENT_RESULT_HANDLED; + } + else if (event.m_id == ACTION_GESTURE_END || event.m_id == ACTION_GESTURE_ABORT) + { // release exclusive access + CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, 0, GetParentID()); + SendWindowMessage(msg); + return EVENT_RESULT_HANDLED; + } + else if (event.m_id == ACTION_GESTURE_PAN) + { // do the drag and validate our offset (corrects for end of scroll) + m_scroller.SetValue(CLAMP(m_scroller.GetValue() - ((m_orientation == HORIZONTAL) ? event.m_offsetX : event.m_offsetY), 0, m_totalSize - Size())); + SetInvalid(); + return EVENT_RESULT_HANDLED; + } + + return EVENT_RESULT_UNHANDLED; +} + +float CGUIControlGroupList::GetTotalSize() const +{ + float totalSize = 0; + for (ciControls it = m_children.begin(); it != m_children.end(); ++it) + { + CGUIControl *control = *it; + if (!control->IsVisible()) continue; + totalSize += Size(control) + m_itemGap; + } + if (totalSize > 0) totalSize -= m_itemGap; + return totalSize; +} |