PageRenderTime 65ms CodeModel.GetById 12ms app.highlight 47ms RepoModel.GetById 1ms app.codeStats 0ms

/xbmc/guilib/GUIControlGroupList.cpp

http://github.com/xbmc/xbmc
C++ | 596 lines | 520 code | 39 blank | 37 comment | 165 complexity | c3d686bf1975f9aedb98c981adc718ad MD5 | raw file
  1/*
  2 *  Copyright (C) 2005-2018 Team Kodi
  3 *  This file is part of Kodi - https://kodi.tv
  4 *
  5 *  SPDX-License-Identifier: GPL-2.0-or-later
  6 *  See LICENSES/README.md for more information.
  7 */
  8
  9#include "GUIControlGroupList.h"
 10
 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"
 18
 19CGUIControlGroupList::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)
 22{
 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;
 33}
 34
 35CGUIControlGroupList::~CGUIControlGroupList(void) = default;
 36
 37void CGUIControlGroupList::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
 38{
 39  if (m_scroller.Update(currentTime))
 40    MarkDirtyRegion();
 41
 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)
 45  {
 46    CGUIControl *control = *it;
 47    GUIPROFILER_VISIBILITY_BEGIN(control);
 48    control->UpdateVisibility(nullptr);
 49    GUIPROFILER_VISIBILITY_END(control);
 50  }
 51
 52  ValidateOffset();
 53  if (m_pageControl && m_lastScrollerValue != m_scroller.GetValue())
 54  {
 55    CGUIMessage message(GUI_MSG_LABEL_RESET, GetParentID(), m_pageControl, (int)Size(), (int)m_totalSize);
 56    SendWindowMessage(message);
 57    CGUIMessage message2(GUI_MSG_ITEM_SELECT, GetParentID(), m_pageControl, (int)m_scroller.GetValue());
 58    SendWindowMessage(message2);
 59    m_lastScrollerValue = static_cast<int>(m_scroller.GetValue());
 60  }
 61  // we run through the controls, rendering as we go
 62  int index = 0;
 63  float pos = GetAlignOffset();
 64  for (iControls it = m_children.begin(); it != m_children.end(); ++it)
 65  {
 66    // note we render all controls, even if they're offscreen, as then they'll be updated
 67    // with respect to animations
 68    CGUIControl *control = *it;
 69    if (m_orientation == VERTICAL)
 70      CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX, m_posY + pos - m_scroller.GetValue());
 71    else
 72      CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX + pos - m_scroller.GetValue(), m_posY);
 73    control->DoProcess(currentTime, dirtyregions);
 74
 75    if (control->IsVisible())
 76    {
 77      if (IsControlOnScreen(pos, control))
 78      {
 79        if (control->HasFocus())
 80          m_focusedPosition = index;
 81        index++;
 82      }
 83
 84      pos += Size(control) + m_itemGap;
 85    }
 86    CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin();
 87  }
 88  CGUIControl::Process(currentTime, dirtyregions);
 89}
 90
 91void CGUIControlGroupList::Render()
 92{
 93  // we run through the controls, rendering as we go
 94  bool render(CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_posX, m_posY, m_width, m_height));
 95  float pos = GetAlignOffset();
 96  float focusedPos = 0;
 97  CGUIControl *focusedControl = NULL;
 98  for (iControls it = m_children.begin(); it != m_children.end(); ++it)
 99  {
100    // note we render all controls, even if they're offscreen, as then they'll be updated
101    // with respect to animations
102    CGUIControl *control = *it;
103    if (m_renderFocusedLast && control->HasFocus())
104    {
105      focusedControl = control;
106      focusedPos = pos;
107    }
108    else
109    {
110      if (m_orientation == VERTICAL)
111        CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX, m_posY + pos - m_scroller.GetValue());
112      else
113        CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX + pos - m_scroller.GetValue(), m_posY);
114      control->DoRender();
115    }
116    if (control->IsVisible())
117      pos += Size(control) + m_itemGap;
118    CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin();
119  }
120  if (focusedControl)
121  {
122    if (m_orientation == VERTICAL)
123      CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX, m_posY + focusedPos - m_scroller.GetValue());
124    else
125      CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(m_posX + focusedPos - m_scroller.GetValue(), m_posY);
126    focusedControl->DoRender();
127  }
128  if (render) CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
129  CGUIControl::Render();
130}
131
132bool CGUIControlGroupList::OnMessage(CGUIMessage& message)
133{
134  switch (message.GetMessage() )
135  {
136  case GUI_MSG_FOCUSED:
137    { // a control has been focused
138      // scroll if we need to and update our page control
139      ValidateOffset();
140      float offset = 0;
141      for (iControls it = m_children.begin(); it != m_children.end(); ++it)
142      {
143        CGUIControl *control = *it;
144        if (!control->IsVisible())
145          continue;
146        if (control->GetControl(message.GetControlId()))
147        {
148          // find out whether this is the first or last control
149          if (IsFirstFocusableControl(control))
150            ScrollTo(0);
151          else if (IsLastFocusableControl(control))
152            ScrollTo(m_totalSize - Size());
153          else if (offset < m_scroller.GetValue())
154            ScrollTo(offset);
155          else if (offset + Size(control) > m_scroller.GetValue() + Size())
156            ScrollTo(offset + Size(control) - Size());
157          break;
158        }
159        offset += Size(control) + m_itemGap;
160      }
161    }
162    break;
163  case GUI_MSG_SETFOCUS:
164    {
165      // we've been asked to focus.  We focus the last control if it's on this page,
166      // else we'll focus the first focusable control from our offset (after verifying it)
167      ValidateOffset();
168      // now check the focusControl's offset
169      float offset = 0;
170      for (iControls it = m_children.begin(); it != m_children.end(); ++it)
171      {
172        CGUIControl *control = *it;
173        if (!control->IsVisible())
174          continue;
175        if (control->GetControl(m_focusedControl))
176        {
177          if (IsControlOnScreen(offset, control))
178            return CGUIControlGroup::OnMessage(message);
179          break;
180        }
181        offset += Size(control) + m_itemGap;
182      }
183      // find the first control on this page
184      offset = 0;
185      for (iControls it = m_children.begin(); it != m_children.end(); ++it)
186      {
187        CGUIControl *control = *it;
188        if (!control->IsVisible())
189          continue;
190        if (control->CanFocus() && IsControlOnScreen(offset, control))
191        {
192          m_focusedControl = control->GetID();
193          break;
194        }
195        offset += Size(control) + m_itemGap;
196      }
197    }
198    break;
199  case GUI_MSG_PAGE_CHANGE:
200    {
201      if (message.GetSenderId() == m_pageControl)
202      { // it's from our page control
203        ScrollTo((float)message.GetParam1());
204        return true;
205      }
206    }
207    break;
208  }
209  return CGUIControlGroup::OnMessage(message);
210}
211
212void CGUIControlGroupList::ValidateOffset()
213{
214  // calculate item gap. this needs to be done
215  // before fetching the total size
216  CalculateItemGap();
217  // calculate how many items we have on this page
218  m_totalSize = GetTotalSize();
219  // check our m_offset range
220  if (m_scroller.GetValue() > m_totalSize - Size())
221    m_scroller.SetValue(m_totalSize - Size());
222  if (m_scroller.GetValue() < 0) m_scroller.SetValue(0);
223}
224
225void CGUIControlGroupList::AddControl(CGUIControl *control, int position /*= -1*/)
226{
227  // NOTE: We override control navigation here, but we don't override the <onleft> etc. builtins
228  //       if specified.
229  if (position < 0 || position > (int)m_children.size()) // add at the end
230    position = (int)m_children.size();
231
232  if (control)
233  { // set the navigation of items so that they form a list
234    CGUIAction beforeAction = GetAction((m_orientation == VERTICAL) ? ACTION_MOVE_UP : ACTION_MOVE_LEFT);
235    CGUIAction afterAction = GetAction((m_orientation == VERTICAL) ? ACTION_MOVE_DOWN : ACTION_MOVE_RIGHT);
236    if (m_children.size())
237    {
238      // we're inserting at the given position, so grab the items above and below and alter
239      // their navigation accordingly
240      CGUIControl *before = NULL;
241      CGUIControl *after = NULL;
242      if (position == 0)
243      { // inserting at the beginning
244        after = m_children[0];
245        if (!afterAction.HasActionsMeetingCondition() || afterAction.GetNavigation() == GetID()) // we're wrapping around bottom->top, so we have to update the last item
246          before = m_children[m_children.size() - 1];
247        if (!beforeAction.HasActionsMeetingCondition() || beforeAction.GetNavigation() == GetID())   // we're wrapping around top->bottom
248          beforeAction = CGUIAction(m_children[m_children.size() - 1]->GetID());
249        afterAction = CGUIAction(after->GetID());
250      }
251      else if (position == (int)m_children.size())
252      { // inserting at the end
253        before = m_children[m_children.size() - 1];
254        if (!beforeAction.HasActionsMeetingCondition() || beforeAction.GetNavigation() == GetID())   // we're wrapping around top->bottom, so we have to update the first item
255          after = m_children[0];
256        if (!afterAction.HasActionsMeetingCondition() || afterAction.GetNavigation() == GetID()) // we're wrapping around bottom->top
257          afterAction = CGUIAction(m_children[0]->GetID());
258        beforeAction = CGUIAction(before->GetID());
259      }
260      else
261      { // inserting somewhere in the middle
262        before = m_children[position - 1];
263        after = m_children[position];
264        beforeAction = CGUIAction(before->GetID());
265        afterAction = CGUIAction(after->GetID());
266      }
267      if (m_orientation == VERTICAL)
268      {
269        if (before) // update the DOWN action to point to us
270          before->SetAction(ACTION_MOVE_DOWN, CGUIAction(control->GetID()));
271        if (after) // update the UP action to point to us
272          after->SetAction(ACTION_MOVE_UP, CGUIAction(control->GetID()));
273      }
274      else
275      {
276        if (before) // update the RIGHT action to point to us
277          before->SetAction(ACTION_MOVE_RIGHT, CGUIAction(control->GetID()));
278        if (after) // update the LEFT action to point to us
279          after->SetAction(ACTION_MOVE_LEFT, CGUIAction(control->GetID()));
280      }
281    }
282    // now the control's nav
283    // set navigation path on orientation axis
284    // and try to apply other nav actions from grouplist
285    // don't override them if child have already defined actions
286    if (m_orientation == VERTICAL)
287    {
288      control->SetAction(ACTION_MOVE_UP, beforeAction);
289      control->SetAction(ACTION_MOVE_DOWN, afterAction);
290      control->SetAction(ACTION_MOVE_LEFT, GetAction(ACTION_MOVE_LEFT), false);
291      control->SetAction(ACTION_MOVE_RIGHT, GetAction(ACTION_MOVE_RIGHT), false);
292    }
293    else
294    {
295      control->SetAction(ACTION_MOVE_LEFT, beforeAction);
296      control->SetAction(ACTION_MOVE_RIGHT, afterAction);
297      control->SetAction(ACTION_MOVE_UP, GetAction(ACTION_MOVE_UP), false);
298      control->SetAction(ACTION_MOVE_DOWN, GetAction(ACTION_MOVE_DOWN), false);
299    }
300    control->SetAction(ACTION_NAV_BACK, GetAction(ACTION_NAV_BACK), false);
301
302    if (!m_useControlPositions)
303      control->SetPosition(0,0);
304    CGUIControlGroup::AddControl(control, position);
305    m_totalSize = GetTotalSize();
306  }
307}
308
309void CGUIControlGroupList::ClearAll()
310{
311  m_totalSize = 0;
312  CGUIControlGroup::ClearAll();
313  m_scroller.SetValue(0);
314}
315
316#define CLAMP(x, low, high)  (((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x)))
317
318float CGUIControlGroupList::GetWidth() const
319{
320  if (m_orientation == HORIZONTAL)
321    return CLAMP(m_totalSize, m_minSize, m_width);
322  return CGUIControlGroup::GetWidth();
323}
324
325float CGUIControlGroupList::GetHeight() const
326{
327  if (m_orientation == VERTICAL)
328    return CLAMP(m_totalSize, m_minSize, m_height);
329  return CGUIControlGroup::GetHeight();
330}
331
332void CGUIControlGroupList::SetMinSize(float minWidth, float minHeight)
333{
334  if (m_orientation == VERTICAL)
335    m_minSize = minHeight;
336  else
337    m_minSize = minWidth;
338}
339
340float CGUIControlGroupList::Size(const CGUIControl *control) const
341{
342  return (m_orientation == VERTICAL) ? control->GetYPosition() + control->GetHeight() : control->GetXPosition() + control->GetWidth();
343}
344
345inline float CGUIControlGroupList::Size() const
346{
347  return (m_orientation == VERTICAL) ? m_height : m_width;
348}
349
350void CGUIControlGroupList::SetInvalid()
351{
352  CGUIControl::SetInvalid();
353  // Force a message to the scrollbar
354  m_lastScrollerValue = -1;
355}
356
357void CGUIControlGroupList::ScrollTo(float offset)
358{
359  m_scroller.ScrollTo(offset);
360  if (m_scroller.IsScrolling())
361    SetInvalid();
362  MarkDirtyRegion();
363}
364
365EVENT_RESULT CGUIControlGroupList::SendMouseEvent(const CPoint &point, const CMouseEvent &event)
366{
367  // transform our position into child coordinates
368  CPoint childPoint(point);
369  m_transform.InverseTransformPosition(childPoint.x, childPoint.y);
370  if (CGUIControl::CanFocus())
371  {
372    float pos = 0;
373    float alignOffset = GetAlignOffset();
374    for (ciControls i = m_children.begin(); i != m_children.end(); ++i)
375    {
376      CGUIControl *child = *i;
377      if (child->IsVisible())
378      {
379        if (IsControlOnScreen(pos, child))
380        { // we're on screen
381          float offsetX = m_orientation == VERTICAL ? m_posX : m_posX + alignOffset + pos - m_scroller.GetValue();
382          float offsetY = m_orientation == VERTICAL ? m_posY + alignOffset + pos - m_scroller.GetValue() : m_posY;
383          EVENT_RESULT ret = child->SendMouseEvent(childPoint - CPoint(offsetX, offsetY), event);
384          if (ret)
385          { // we've handled the action, and/or have focused an item
386            return ret;
387          }
388        }
389        pos += Size(child) + m_itemGap;
390      }
391    }
392    // none of our children want the event, but we may want it.
393    EVENT_RESULT ret;
394    if (HitTest(childPoint) && (ret = OnMouseEvent(childPoint, event)))
395      return ret;
396  }
397  m_focusedControl = 0;
398  return EVENT_RESULT_UNHANDLED;
399}
400
401void CGUIControlGroupList::UnfocusFromPoint(const CPoint &point)
402{
403  float pos = 0;
404  CPoint controlCoords(point);
405  m_transform.InverseTransformPosition(controlCoords.x, controlCoords.y);
406  float alignOffset = GetAlignOffset();
407  for (iControls it = m_children.begin(); it != m_children.end(); ++it)
408  {
409    CGUIControl *child = *it;
410    if (child->IsVisible())
411    {
412      if (IsControlOnScreen(pos, child))
413      { // we're on screen
414        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);
415        child->UnfocusFromPoint(controlCoords - offset);
416      }
417      pos += Size(child) + m_itemGap;
418    }
419  }
420  CGUIControl::UnfocusFromPoint(point);
421}
422
423bool CGUIControlGroupList::GetCondition(int condition, int data) const
424{
425  switch (condition)
426  {
427  case CONTAINER_HAS_NEXT:
428    return (m_totalSize >= Size() && m_scroller.GetValue() < m_totalSize - Size());
429  case CONTAINER_HAS_PREVIOUS:
430    return (m_scroller.GetValue() > 0);
431  case CONTAINER_POSITION:
432    return (m_focusedPosition == data);
433  default:
434    return false;
435  }
436}
437
438std::string CGUIControlGroupList::GetLabel(int info) const
439{
440  switch (info)
441  {
442  case CONTAINER_CURRENT_ITEM:
443    return StringUtils::Format("%i", GetSelectedItem());
444  case CONTAINER_NUM_ITEMS:
445    return StringUtils::Format("%i", GetNumItems());
446  case CONTAINER_POSITION:
447    return StringUtils::Format("%i", m_focusedPosition);
448  default:
449    break;
450  }
451  return "";
452}
453
454int CGUIControlGroupList::GetNumItems() const
455{
456  return std::count_if(m_children.begin(), m_children.end(), [&](const CGUIControl *child) {
457    return (child->IsVisible() && child->CanFocus());
458  });
459}
460
461int CGUIControlGroupList::GetSelectedItem() const
462{
463  int index = 1;
464  for (const auto& child : m_children)
465  {
466    if (child->IsVisible() && child->CanFocus())
467    {
468      if (child->HasFocus())
469        return index;
470      index++;
471    }
472  }
473  return -1;
474}
475
476bool CGUIControlGroupList::IsControlOnScreen(float pos, const CGUIControl *control) const
477{
478  return (pos >= m_scroller.GetValue() && pos + Size(control) <= m_scroller.GetValue() + Size());
479}
480
481bool CGUIControlGroupList::IsFirstFocusableControl(const CGUIControl *control) const
482{
483  for (ciControls it = m_children.begin(); it != m_children.end(); ++it)
484  {
485    CGUIControl *child = *it;
486    if (child->IsVisible() && child->CanFocus())
487    { // found first focusable
488      return child == control;
489    }
490  }
491  return false;
492}
493
494bool CGUIControlGroupList::IsLastFocusableControl(const CGUIControl *control) const
495{
496  for (crControls it = m_children.rbegin(); it != m_children.rend(); ++it)
497  {
498    CGUIControl *child = *it;
499    if (child->IsVisible() && child->CanFocus())
500    { // found first focusable
501      return child == control;
502    }
503  }
504  return false;
505}
506
507void CGUIControlGroupList::CalculateItemGap()
508{
509  if (m_alignment & XBFONT_JUSTIFIED)
510  {
511    int itemsCount = 0;
512    float itemsSize = 0;
513    for (const auto& child : m_children)
514    {
515      if (child->IsVisible())
516      {
517        itemsSize += Size(child);
518        itemsCount++;
519      }
520    }
521
522    if (itemsCount > 0)
523      m_itemGap = (Size() - itemsSize) / itemsCount;
524  }
525}
526
527float CGUIControlGroupList::GetAlignOffset() const
528{
529  if (m_totalSize < Size())
530  {
531    if (m_alignment & XBFONT_RIGHT)
532      return Size() - m_totalSize;
533    if (m_alignment & (XBFONT_CENTER_X | XBFONT_JUSTIFIED))
534      return (Size() - m_totalSize)*0.5f;
535  }
536  return 0.0f;
537}
538
539EVENT_RESULT CGUIControlGroupList::OnMouseEvent(const CPoint &point, const CMouseEvent &event)
540{
541  if (event.m_id == ACTION_MOUSE_WHEEL_UP || event.m_id == ACTION_MOUSE_WHEEL_DOWN)
542  {
543    // find the current control and move to the next or previous
544    float offset = 0;
545    for (ciControls it = m_children.begin(); it != m_children.end(); ++it)
546    {
547      CGUIControl *control = *it;
548      if (!control->IsVisible()) continue;
549      float nextOffset = offset + Size(control) + m_itemGap;
550      if (event.m_id == ACTION_MOUSE_WHEEL_DOWN && nextOffset > m_scroller.GetValue() && m_scroller.GetValue() < m_totalSize - Size()) // past our current offset
551      {
552        ScrollTo(nextOffset);
553        return EVENT_RESULT_HANDLED;
554      }
555      else if (event.m_id == ACTION_MOUSE_WHEEL_UP && nextOffset >= m_scroller.GetValue() && m_scroller.GetValue() > 0) // at least at our current offset
556      {
557        ScrollTo(offset);
558        return EVENT_RESULT_HANDLED;
559      }
560      offset = nextOffset;
561    }
562  }
563  else if (event.m_id == ACTION_GESTURE_BEGIN)
564  { // grab exclusive access
565    CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, GetID(), GetParentID());
566    SendWindowMessage(msg);
567    return EVENT_RESULT_HANDLED;
568  }
569  else if (event.m_id == ACTION_GESTURE_END || event.m_id == ACTION_GESTURE_ABORT)
570  { // release exclusive access
571    CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, 0, GetParentID());
572    SendWindowMessage(msg);
573    return EVENT_RESULT_HANDLED;
574  }
575  else if (event.m_id == ACTION_GESTURE_PAN)
576  { // do the drag and validate our offset (corrects for end of scroll)
577    m_scroller.SetValue(CLAMP(m_scroller.GetValue() - ((m_orientation == HORIZONTAL) ? event.m_offsetX : event.m_offsetY), 0, m_totalSize - Size()));
578    SetInvalid();
579    return EVENT_RESULT_HANDLED;
580  }
581
582  return EVENT_RESULT_UNHANDLED;
583}
584
585float CGUIControlGroupList::GetTotalSize() const
586{
587  float totalSize = 0;
588  for (ciControls it = m_children.begin(); it != m_children.end(); ++it)
589  {
590    CGUIControl *control = *it;
591    if (!control->IsVisible()) continue;
592    totalSize += Size(control) + m_itemGap;
593  }
594  if (totalSize > 0) totalSize -= m_itemGap;
595  return totalSize;
596}