Merge pull request #22816 from CastagnaIT/fix_tx3g
[xbmc.git] / xbmc / guilib / GUIControlGroupList.cpp
blob5252d67dbf4c8e55d509d836b71b66b504318e94
1 /*
2 * Copyright (C) 2005-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
7 */
9 #include "GUIControlGroupList.h"
11 #include "GUIAction.h"
12 #include "GUIControlProfiler.h"
13 #include "GUIFont.h" // for XBFONT_* definitions
14 #include "GUIMessage.h"
15 #include "guilib/guiinfo/GUIInfoLabels.h"
16 #include "input/Key.h"
17 #include "utils/StringUtils.h"
19 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)
20 : CGUIControlGroup(parentID, controlID, posX, posY, width, height)
21 , m_scroller(scroller)
23 m_itemGap = itemGap;
24 m_pageControl = pageControl;
25 m_focusedPosition = 0;
26 m_totalSize = 0;
27 m_orientation = orientation;
28 m_alignment = alignment;
29 m_lastScrollerValue = -1;
30 m_useControlPositions = useControlPositions;
31 ControlType = GUICONTROL_GROUPLIST;
32 m_minSize = 0;
35 CGUIControlGroupList::~CGUIControlGroupList(void) = default;
37 void CGUIControlGroupList::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
39 if (m_scroller.Update(currentTime))
40 MarkDirtyRegion();
42 // first we update visibility of all our items, to ensure our size and
43 // alignment computations are correct.
44 for (iControls it = m_children.begin(); it != m_children.end(); ++it)
46 CGUIControl *control = *it;
47 GUIPROFILER_VISIBILITY_BEGIN(control);
48 control->UpdateVisibility(nullptr);
49 GUIPROFILER_VISIBILITY_END(control);
52 // visibility status of some of the list items may have changed. Thus, the group list size
53 // may now be different and the scroller needs to be updated
54 int previousTotalSize = m_totalSize;
55 ValidateOffset(); // m_totalSize is updated here
56 bool sizeChanged = previousTotalSize != m_totalSize;
58 if (m_pageControl && (m_lastScrollerValue != m_scroller.GetValue() || sizeChanged))
60 CGUIMessage message(GUI_MSG_LABEL_RESET, GetParentID(), m_pageControl, (int)Size(), (int)m_totalSize);
61 SendWindowMessage(message);
62 CGUIMessage message2(GUI_MSG_ITEM_SELECT, GetParentID(), m_pageControl, (int)m_scroller.GetValue());
63 SendWindowMessage(message2);
64 m_lastScrollerValue = static_cast<int>(m_scroller.GetValue());
66 // we run through the controls, rendering as we go
67 int index = 0;
68 float pos = GetAlignOffset();
69 for (iControls it = m_children.begin(); it != m_children.end(); ++it)
71 // note we render all controls, even if they're offscreen, as then they'll be updated
72 // with respect to animations
73 CGUIControl *control = *it;
74 if (m_orientation == VERTICAL)
75 CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX, m_posY + pos - m_scroller.GetValue());
76 else
77 CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX + pos - m_scroller.GetValue(), m_posY);
78 control->DoProcess(currentTime, dirtyregions);
80 if (control->IsVisible())
82 if (IsControlOnScreen(pos, control))
84 if (control->HasFocus())
85 m_focusedPosition = index;
86 index++;
89 pos += Size(control) + m_itemGap;
91 CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin();
93 CGUIControl::Process(currentTime, dirtyregions);
96 void CGUIControlGroupList::Render()
98 // we run through the controls, rendering as we go
99 bool render(CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_posX, m_posY, m_width, m_height));
100 float pos = GetAlignOffset();
101 float focusedPos = 0;
102 CGUIControl *focusedControl = NULL;
103 for (iControls it = m_children.begin(); it != m_children.end(); ++it)
105 // note we render all controls, even if they're offscreen, as then they'll be updated
106 // with respect to animations
107 CGUIControl *control = *it;
108 if (m_renderFocusedLast && control->HasFocus())
110 focusedControl = control;
111 focusedPos = pos;
113 else
115 if (m_orientation == VERTICAL)
116 CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX, m_posY + pos - m_scroller.GetValue());
117 else
118 CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX + pos - m_scroller.GetValue(), m_posY);
119 control->DoRender();
121 if (control->IsVisible())
122 pos += Size(control) + m_itemGap;
123 CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin();
125 if (focusedControl)
127 if (m_orientation == VERTICAL)
128 CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX, m_posY + focusedPos - m_scroller.GetValue());
129 else
130 CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX + focusedPos - m_scroller.GetValue(), m_posY);
131 focusedControl->DoRender();
133 if (render) CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
134 CGUIControl::Render();
137 bool CGUIControlGroupList::OnMessage(CGUIMessage& message)
139 switch (message.GetMessage() )
141 case GUI_MSG_FOCUSED:
142 { // a control has been focused
143 // scroll if we need to and update our page control
144 ValidateOffset();
145 float offset = 0;
146 for (iControls it = m_children.begin(); it != m_children.end(); ++it)
148 CGUIControl *control = *it;
149 if (!control->IsVisible())
150 continue;
151 if (control->GetControl(message.GetControlId()))
153 // find out whether this is the first or last control
154 if (IsFirstFocusableControl(control))
155 ScrollTo(0);
156 else if (IsLastFocusableControl(control))
157 ScrollTo(m_totalSize - Size());
158 else if (offset < m_scroller.GetValue())
159 ScrollTo(offset);
160 else if (offset + Size(control) > m_scroller.GetValue() + Size())
161 ScrollTo(offset + Size(control) - Size());
162 break;
164 offset += Size(control) + m_itemGap;
167 break;
168 case GUI_MSG_SETFOCUS:
170 // we've been asked to focus. We focus the last control if it's on this page,
171 // else we'll focus the first focusable control from our offset (after verifying it)
172 ValidateOffset();
173 // now check the focusControl's offset
174 float offset = 0;
175 for (iControls it = m_children.begin(); it != m_children.end(); ++it)
177 CGUIControl *control = *it;
178 if (!control->IsVisible())
179 continue;
180 if (control->GetControl(m_focusedControl))
182 if (IsControlOnScreen(offset, control))
183 return CGUIControlGroup::OnMessage(message);
184 break;
186 offset += Size(control) + m_itemGap;
188 // find the first control on this page
189 offset = 0;
190 for (iControls it = m_children.begin(); it != m_children.end(); ++it)
192 CGUIControl *control = *it;
193 if (!control->IsVisible())
194 continue;
195 if (control->CanFocus() && IsControlOnScreen(offset, control))
197 m_focusedControl = control->GetID();
198 break;
200 offset += Size(control) + m_itemGap;
203 break;
204 case GUI_MSG_PAGE_CHANGE:
206 if (message.GetSenderId() == m_pageControl)
207 { // it's from our page control
208 ScrollTo((float)message.GetParam1());
209 return true;
212 break;
214 return CGUIControlGroup::OnMessage(message);
217 void CGUIControlGroupList::ValidateOffset()
219 // calculate item gap. this needs to be done
220 // before fetching the total size
221 CalculateItemGap();
222 // calculate how many items we have on this page
223 m_totalSize = GetTotalSize();
224 // check our m_offset range
225 if (m_scroller.GetValue() > m_totalSize - Size())
226 m_scroller.SetValue(m_totalSize - Size());
227 if (m_scroller.GetValue() < 0) m_scroller.SetValue(0);
230 void CGUIControlGroupList::AddControl(CGUIControl *control, int position /*= -1*/)
232 // NOTE: We override control navigation here, but we don't override the <onleft> etc. builtins
233 // if specified.
234 if (position < 0 || position > (int)m_children.size()) // add at the end
235 position = (int)m_children.size();
237 if (control)
238 { // set the navigation of items so that they form a list
239 CGUIAction beforeAction = GetAction((m_orientation == VERTICAL) ? ACTION_MOVE_UP : ACTION_MOVE_LEFT);
240 CGUIAction afterAction = GetAction((m_orientation == VERTICAL) ? ACTION_MOVE_DOWN : ACTION_MOVE_RIGHT);
241 if (m_children.size())
243 // we're inserting at the given position, so grab the items above and below and alter
244 // their navigation accordingly
245 CGUIControl *before = NULL;
246 CGUIControl *after = NULL;
247 if (position == 0)
248 { // inserting at the beginning
249 after = m_children[0];
250 if (!afterAction.HasActionsMeetingCondition() || afterAction.GetNavigation() == GetID()) // we're wrapping around bottom->top, so we have to update the last item
251 before = m_children[m_children.size() - 1];
252 if (!beforeAction.HasActionsMeetingCondition() || beforeAction.GetNavigation() == GetID()) // we're wrapping around top->bottom
253 beforeAction = CGUIAction(m_children[m_children.size() - 1]->GetID());
254 afterAction = CGUIAction(after->GetID());
256 else if (position == (int)m_children.size())
257 { // inserting at the end
258 before = m_children[m_children.size() - 1];
259 if (!beforeAction.HasActionsMeetingCondition() || beforeAction.GetNavigation() == GetID()) // we're wrapping around top->bottom, so we have to update the first item
260 after = m_children[0];
261 if (!afterAction.HasActionsMeetingCondition() || afterAction.GetNavigation() == GetID()) // we're wrapping around bottom->top
262 afterAction = CGUIAction(m_children[0]->GetID());
263 beforeAction = CGUIAction(before->GetID());
265 else
266 { // inserting somewhere in the middle
267 before = m_children[position - 1];
268 after = m_children[position];
269 beforeAction = CGUIAction(before->GetID());
270 afterAction = CGUIAction(after->GetID());
272 if (m_orientation == VERTICAL)
274 if (before) // update the DOWN action to point to us
275 before->SetAction(ACTION_MOVE_DOWN, CGUIAction(control->GetID()));
276 if (after) // update the UP action to point to us
277 after->SetAction(ACTION_MOVE_UP, CGUIAction(control->GetID()));
279 else
281 if (before) // update the RIGHT action to point to us
282 before->SetAction(ACTION_MOVE_RIGHT, CGUIAction(control->GetID()));
283 if (after) // update the LEFT action to point to us
284 after->SetAction(ACTION_MOVE_LEFT, CGUIAction(control->GetID()));
287 // now the control's nav
288 // set navigation path on orientation axis
289 // and try to apply other nav actions from grouplist
290 // don't override them if child have already defined actions
291 if (m_orientation == VERTICAL)
293 control->SetAction(ACTION_MOVE_UP, beforeAction);
294 control->SetAction(ACTION_MOVE_DOWN, afterAction);
295 control->SetAction(ACTION_MOVE_LEFT, GetAction(ACTION_MOVE_LEFT), false);
296 control->SetAction(ACTION_MOVE_RIGHT, GetAction(ACTION_MOVE_RIGHT), false);
298 else
300 control->SetAction(ACTION_MOVE_LEFT, beforeAction);
301 control->SetAction(ACTION_MOVE_RIGHT, afterAction);
302 control->SetAction(ACTION_MOVE_UP, GetAction(ACTION_MOVE_UP), false);
303 control->SetAction(ACTION_MOVE_DOWN, GetAction(ACTION_MOVE_DOWN), false);
305 control->SetAction(ACTION_NAV_BACK, GetAction(ACTION_NAV_BACK), false);
307 if (!m_useControlPositions)
308 control->SetPosition(0,0);
309 CGUIControlGroup::AddControl(control, position);
310 m_totalSize = GetTotalSize();
314 void CGUIControlGroupList::ClearAll()
316 m_totalSize = 0;
317 CGUIControlGroup::ClearAll();
318 m_scroller.SetValue(0);
321 #define CLAMP(x, low, high) (((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x)))
323 float CGUIControlGroupList::GetWidth() const
325 if (m_orientation == HORIZONTAL)
326 return CLAMP(m_totalSize, m_minSize, m_width);
327 return CGUIControlGroup::GetWidth();
330 float CGUIControlGroupList::GetHeight() const
332 if (m_orientation == VERTICAL)
333 return CLAMP(m_totalSize, m_minSize, m_height);
334 return CGUIControlGroup::GetHeight();
337 void CGUIControlGroupList::SetMinSize(float minWidth, float minHeight)
339 if (m_orientation == VERTICAL)
340 m_minSize = minHeight;
341 else
342 m_minSize = minWidth;
345 float CGUIControlGroupList::Size(const CGUIControl *control) const
347 return (m_orientation == VERTICAL) ? control->GetYPosition() + control->GetHeight() : control->GetXPosition() + control->GetWidth();
350 inline float CGUIControlGroupList::Size() const
352 return (m_orientation == VERTICAL) ? m_height : m_width;
355 void CGUIControlGroupList::SetInvalid()
357 CGUIControl::SetInvalid();
358 // Force a message to the scrollbar
359 m_lastScrollerValue = -1;
362 void CGUIControlGroupList::ScrollTo(float offset)
364 m_scroller.ScrollTo(offset);
365 if (m_scroller.IsScrolling())
366 SetInvalid();
367 MarkDirtyRegion();
370 EVENT_RESULT CGUIControlGroupList::SendMouseEvent(const CPoint &point, const CMouseEvent &event)
372 // transform our position into child coordinates
373 CPoint childPoint(point);
374 m_transform.InverseTransformPosition(childPoint.x, childPoint.y);
375 if (CGUIControl::CanFocus())
377 float pos = 0;
378 float alignOffset = GetAlignOffset();
379 for (ciControls i = m_children.begin(); i != m_children.end(); ++i)
381 CGUIControl *child = *i;
382 if (child->IsVisible())
384 if (IsControlOnScreen(pos, child))
385 { // we're on screen
386 float offsetX = m_orientation == VERTICAL ? m_posX : m_posX + alignOffset + pos - m_scroller.GetValue();
387 float offsetY = m_orientation == VERTICAL ? m_posY + alignOffset + pos - m_scroller.GetValue() : m_posY;
388 EVENT_RESULT ret = child->SendMouseEvent(childPoint - CPoint(offsetX, offsetY), event);
389 if (ret)
390 { // we've handled the action, and/or have focused an item
391 return ret;
394 pos += Size(child) + m_itemGap;
397 // none of our children want the event, but we may want it.
398 EVENT_RESULT ret;
399 if (HitTest(childPoint) && (ret = OnMouseEvent(childPoint, event)))
400 return ret;
402 m_focusedControl = 0;
403 return EVENT_RESULT_UNHANDLED;
406 void CGUIControlGroupList::UnfocusFromPoint(const CPoint &point)
408 float pos = 0;
409 CPoint controlCoords(point);
410 m_transform.InverseTransformPosition(controlCoords.x, controlCoords.y);
411 float alignOffset = GetAlignOffset();
412 for (iControls it = m_children.begin(); it != m_children.end(); ++it)
414 CGUIControl *child = *it;
415 if (child->IsVisible())
417 if (IsControlOnScreen(pos, child))
418 { // we're on screen
419 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);
420 child->UnfocusFromPoint(controlCoords - offset);
422 pos += Size(child) + m_itemGap;
425 CGUIControl::UnfocusFromPoint(point);
428 bool CGUIControlGroupList::GetCondition(int condition, int data) const
430 switch (condition)
432 case CONTAINER_HAS_NEXT:
433 return (m_totalSize >= Size() && m_scroller.GetValue() < m_totalSize - Size());
434 case CONTAINER_HAS_PREVIOUS:
435 return (m_scroller.GetValue() > 0);
436 case CONTAINER_POSITION:
437 return (m_focusedPosition == data);
438 default:
439 return false;
443 std::string CGUIControlGroupList::GetLabel(int info) const
445 switch (info)
447 case CONTAINER_CURRENT_ITEM:
448 return std::to_string(GetSelectedItem());
449 case CONTAINER_NUM_ITEMS:
450 return std::to_string(GetNumItems());
451 case CONTAINER_POSITION:
452 return std::to_string(m_focusedPosition);
453 default:
454 break;
456 return "";
459 int CGUIControlGroupList::GetNumItems() const
461 return std::count_if(m_children.begin(), m_children.end(), [&](const CGUIControl *child) {
462 return (child->IsVisible() && child->CanFocus());
466 int CGUIControlGroupList::GetSelectedItem() const
468 int index = 1;
469 for (const auto& child : m_children)
471 if (child->IsVisible() && child->CanFocus())
473 if (child->HasFocus())
474 return index;
475 index++;
478 return -1;
481 bool CGUIControlGroupList::IsControlOnScreen(float pos, const CGUIControl *control) const
483 return (pos >= m_scroller.GetValue() && pos + Size(control) <= m_scroller.GetValue() + Size());
486 bool CGUIControlGroupList::IsFirstFocusableControl(const CGUIControl *control) const
488 for (ciControls it = m_children.begin(); it != m_children.end(); ++it)
490 CGUIControl *child = *it;
491 if (child->IsVisible() && child->CanFocus())
492 { // found first focusable
493 return child == control;
496 return false;
499 bool CGUIControlGroupList::IsLastFocusableControl(const CGUIControl *control) const
501 for (crControls it = m_children.rbegin(); it != m_children.rend(); ++it)
503 CGUIControl *child = *it;
504 if (child->IsVisible() && child->CanFocus())
505 { // found first focusable
506 return child == control;
509 return false;
512 void CGUIControlGroupList::CalculateItemGap()
514 if (m_alignment & XBFONT_JUSTIFIED)
516 int itemsCount = 0;
517 float itemsSize = 0;
518 for (const auto& child : m_children)
520 if (child->IsVisible())
522 itemsSize += Size(child);
523 itemsCount++;
527 if (itemsCount > 0)
528 m_itemGap = (Size() - itemsSize) / itemsCount;
532 float CGUIControlGroupList::GetAlignOffset() const
534 if (m_totalSize < Size())
536 if (m_alignment & XBFONT_RIGHT)
537 return Size() - m_totalSize;
538 if (m_alignment & (XBFONT_CENTER_X | XBFONT_JUSTIFIED))
539 return (Size() - m_totalSize)*0.5f;
541 return 0.0f;
544 EVENT_RESULT CGUIControlGroupList::OnMouseEvent(const CPoint &point, const CMouseEvent &event)
546 if (event.m_id == ACTION_MOUSE_WHEEL_UP || event.m_id == ACTION_MOUSE_WHEEL_DOWN)
548 // find the current control and move to the next or previous
549 float offset = 0;
550 for (ciControls it = m_children.begin(); it != m_children.end(); ++it)
552 CGUIControl *control = *it;
553 if (!control->IsVisible()) continue;
554 float nextOffset = offset + Size(control) + m_itemGap;
555 if (event.m_id == ACTION_MOUSE_WHEEL_DOWN && nextOffset > m_scroller.GetValue() && m_scroller.GetValue() < m_totalSize - Size()) // past our current offset
557 ScrollTo(nextOffset);
558 return EVENT_RESULT_HANDLED;
560 else if (event.m_id == ACTION_MOUSE_WHEEL_UP && nextOffset >= m_scroller.GetValue() && m_scroller.GetValue() > 0) // at least at our current offset
562 ScrollTo(offset);
563 return EVENT_RESULT_HANDLED;
565 offset = nextOffset;
568 else if (event.m_id == ACTION_GESTURE_BEGIN)
569 { // grab exclusive access
570 CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, GetID(), GetParentID());
571 SendWindowMessage(msg);
572 return EVENT_RESULT_HANDLED;
574 else if (event.m_id == ACTION_GESTURE_END || event.m_id == ACTION_GESTURE_ABORT)
575 { // release exclusive access
576 CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, 0, GetParentID());
577 SendWindowMessage(msg);
578 return EVENT_RESULT_HANDLED;
580 else if (event.m_id == ACTION_GESTURE_PAN)
581 { // do the drag and validate our offset (corrects for end of scroll)
582 m_scroller.SetValue(CLAMP(m_scroller.GetValue() - ((m_orientation == HORIZONTAL) ? event.m_offsetX : event.m_offsetY), 0, m_totalSize - Size()));
583 SetInvalid();
584 return EVENT_RESULT_HANDLED;
587 return EVENT_RESULT_UNHANDLED;
590 float CGUIControlGroupList::GetTotalSize() const
592 float totalSize = 0;
593 for (ciControls it = m_children.begin(); it != m_children.end(); ++it)
595 CGUIControl *control = *it;
596 if (!control->IsVisible()) continue;
597 totalSize += Size(control) + m_itemGap;
599 if (totalSize > 0) totalSize -= m_itemGap;
600 return totalSize;