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