summaryrefslogtreecommitdiffstats
path: root/xbmc/guilib/GUIControlGroupList.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/guilib/GUIControlGroupList.cpp')
-rw-r--r--xbmc/guilib/GUIControlGroupList.cpp601
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;
+}