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.
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
)
24 m_pageControl
= pageControl
;
25 m_focusedPosition
= 0;
27 m_orientation
= orientation
;
28 m_alignment
= alignment
;
29 m_lastScrollerValue
= -1;
30 m_useControlPositions
= useControlPositions
;
31 ControlType
= GUICONTROL_GROUPLIST
;
35 CGUIControlGroupList::~CGUIControlGroupList(void) = default;
37 void CGUIControlGroupList::Process(unsigned int currentTime
, CDirtyRegionList
&dirtyregions
)
39 if (m_scroller
.Update(currentTime
))
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
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());
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
;
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
;
115 if (m_orientation
== VERTICAL
)
116 CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX
, m_posY
+ pos
- m_scroller
.GetValue());
118 CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX
+ pos
- m_scroller
.GetValue(), m_posY
);
121 if (control
->IsVisible())
122 pos
+= Size(control
) + m_itemGap
;
123 CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin();
127 if (m_orientation
== VERTICAL
)
128 CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX
, m_posY
+ focusedPos
- m_scroller
.GetValue());
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
146 for (iControls it
= m_children
.begin(); it
!= m_children
.end(); ++it
)
148 CGUIControl
*control
= *it
;
149 if (!control
->IsVisible())
151 if (control
->GetControl(message
.GetControlId()))
153 // find out whether this is the first or last control
154 if (IsFirstFocusableControl(control
))
156 else if (IsLastFocusableControl(control
))
157 ScrollTo(m_totalSize
- Size());
158 else if (offset
< m_scroller
.GetValue())
160 else if (offset
+ Size(control
) > m_scroller
.GetValue() + Size())
161 ScrollTo(offset
+ Size(control
) - Size());
164 offset
+= Size(control
) + m_itemGap
;
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)
173 // now check the focusControl's offset
175 for (iControls it
= m_children
.begin(); it
!= m_children
.end(); ++it
)
177 CGUIControl
*control
= *it
;
178 if (!control
->IsVisible())
180 if (control
->GetControl(m_focusedControl
))
182 if (IsControlOnScreen(offset
, control
))
183 return CGUIControlGroup::OnMessage(message
);
186 offset
+= Size(control
) + m_itemGap
;
188 // find the first control on this page
190 for (iControls it
= m_children
.begin(); it
!= m_children
.end(); ++it
)
192 CGUIControl
*control
= *it
;
193 if (!control
->IsVisible())
195 if (control
->CanFocus() && IsControlOnScreen(offset
, control
))
197 m_focusedControl
= control
->GetID();
200 offset
+= Size(control
) + m_itemGap
;
204 case GUI_MSG_PAGE_CHANGE
:
206 if (message
.GetSenderId() == m_pageControl
)
207 { // it's from our page control
208 ScrollTo((float)message
.GetParam1());
214 return CGUIControlGroup::OnMessage(message
);
217 void CGUIControlGroupList::ValidateOffset()
219 // calculate item gap. this needs to be done
220 // before fetching the total size
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
234 if (position
< 0 || position
> (int)m_children
.size()) // add at the end
235 position
= (int)m_children
.size();
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
;
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());
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()));
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);
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()
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
;
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())
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())
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
))
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
);
390 { // we've handled the action, and/or have focused an item
394 pos
+= Size(child
) + m_itemGap
;
397 // none of our children want the event, but we may want it.
399 if (HitTest(childPoint
) && (ret
= OnMouseEvent(childPoint
, event
)))
402 m_focusedControl
= 0;
403 return EVENT_RESULT_UNHANDLED
;
406 void CGUIControlGroupList::UnfocusFromPoint(const CPoint
&point
)
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
))
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
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
);
443 std::string
CGUIControlGroupList::GetLabel(int info
) const
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
);
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
469 for (const auto& child
: m_children
)
471 if (child
->IsVisible() && child
->CanFocus())
473 if (child
->HasFocus())
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
;
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
;
512 void CGUIControlGroupList::CalculateItemGap()
514 if (m_alignment
& XBFONT_JUSTIFIED
)
518 for (const auto& child
: m_children
)
520 if (child
->IsVisible())
522 itemsSize
+= Size(child
);
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
;
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
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
563 return EVENT_RESULT_HANDLED
;
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()));
584 return EVENT_RESULT_HANDLED
;
587 return EVENT_RESULT_UNHANDLED
;
590 float CGUIControlGroupList::GetTotalSize() const
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
;