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 "GUIPanelContainer.h"
10 
11 #include "FileItem.h"
12 #include "GUIListItemLayout.h"
13 #include "GUIMessage.h"
14 #include "guilib/guiinfo/GUIInfoLabels.h"
15 #include "input/Key.h"
16 #include "utils/StringUtils.h"
17 
18 #include <cassert>
19 
CGUIPanelContainer(int parentID,int controlID,float posX,float posY,float width,float height,ORIENTATION orientation,const CScroller & scroller,int preloadItems)20 CGUIPanelContainer::CGUIPanelContainer(int parentID, int controlID, float posX, float posY, float width, float height, ORIENTATION orientation, const CScroller& scroller, int preloadItems)
21     : CGUIBaseContainer(parentID, controlID, posX, posY, width, height, orientation, scroller, preloadItems)
22 {
23   ControlType = GUICONTAINER_PANEL;
24   m_type = VIEW_TYPE_ICON;
25   m_itemsPerRow = 1;
26 }
27 
28 CGUIPanelContainer::~CGUIPanelContainer(void) = default;
29 
Process(unsigned int currentTime,CDirtyRegionList & dirtyregions)30 void CGUIPanelContainer::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions)
31 {
32   ValidateOffset();
33 
34   if (m_bInvalidated)
35     UpdateLayout();
36 
37   if (!m_layout || !m_focusedLayout)
38     return;
39 
40   UpdateScrollOffset(currentTime);
41 
42   int offset = (int)(m_scroller.GetValue() / m_layout->Size(m_orientation));
43 
44   int cacheBefore, cacheAfter;
45   GetCacheOffsets(cacheBefore, cacheAfter);
46 
47   // Free memory not used on screen
48   if ((int)m_items.size() > m_itemsPerPage + cacheBefore + cacheAfter)
49     FreeMemory(CorrectOffset(offset - cacheBefore, 0), CorrectOffset(offset + m_itemsPerPage + 1 + cacheAfter, 0));
50 
51   CPoint origin = CPoint(m_posX, m_posY) + m_renderOffset;
52   float pos = (m_orientation == VERTICAL) ? origin.y : origin.x;
53   float end = (m_orientation == VERTICAL) ? m_posY + m_height : m_posX + m_width;
54   pos += (offset - cacheBefore) * m_layout->Size(m_orientation) - m_scroller.GetValue();
55   end += cacheAfter * m_layout->Size(m_orientation);
56 
57   int current = (offset - cacheBefore) * m_itemsPerRow;
58   int col = 0;
59   while (pos < end && m_items.size())
60   {
61     if (current >= (int)m_items.size())
62       break;
63     if (current >= 0)
64     {
65       CGUIListItemPtr item = m_items[current];
66       item->SetCurrentItem(current + 1);
67       bool focused = (current == GetOffset() * m_itemsPerRow + GetCursor()) && m_bHasFocus;
68 
69       if (m_orientation == VERTICAL)
70         ProcessItem(origin.x + col * m_layout->Size(HORIZONTAL), pos, item, focused, currentTime, dirtyregions);
71       else
72         ProcessItem(pos, origin.y + col * m_layout->Size(VERTICAL), item, focused, currentTime, dirtyregions);
73     }
74     // increment our position
75     if (col < m_itemsPerRow - 1)
76       col++;
77     else
78     {
79       pos += m_layout->Size(m_orientation);
80       col = 0;
81     }
82     current++;
83   }
84 
85   // when we are scrolling up, offset will become lower (integer division, see offset calc)
86   // to have same behaviour when scrolling down, we need to set page control to offset+1
87   UpdatePageControl(offset + (m_scroller.IsScrollingDown() ? 1 : 0));
88 
89   CGUIControl::Process(currentTime, dirtyregions);
90 }
91 
92 
Render()93 void CGUIPanelContainer::Render()
94 {
95   if (!m_layout || !m_focusedLayout)
96     return;
97 
98   int offset = (int)(m_scroller.GetValue() / m_layout->Size(m_orientation));
99 
100   int cacheBefore, cacheAfter;
101   GetCacheOffsets(cacheBefore, cacheAfter);
102 
103   if (CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_posX, m_posY, m_width, m_height))
104   {
105     CPoint origin = CPoint(m_posX, m_posY) + m_renderOffset;
106     float pos = (m_orientation == VERTICAL) ? origin.y : origin.x;
107     float end = (m_orientation == VERTICAL) ? m_posY + m_height : m_posX + m_width;
108     pos += (offset - cacheBefore) * m_layout->Size(m_orientation) - m_scroller.GetValue();
109     end += cacheAfter * m_layout->Size(m_orientation);
110 
111     float focusedPos = 0;
112     int focusedCol = 0;
113     CGUIListItemPtr focusedItem;
114     int current = (offset - cacheBefore) * m_itemsPerRow;
115     int col = 0;
116     while (pos < end && m_items.size())
117     {
118       if (current >= (int)m_items.size())
119         break;
120       if (current >= 0)
121       {
122         CGUIListItemPtr item = m_items[current];
123         bool focused = (current == GetOffset() * m_itemsPerRow + GetCursor()) && m_bHasFocus;
124         // render our item
125         if (focused)
126         {
127           focusedPos = pos;
128           focusedCol = col;
129           focusedItem = item;
130         }
131         else
132         {
133           if (m_orientation == VERTICAL)
134             RenderItem(origin.x + col * m_layout->Size(HORIZONTAL), pos, item.get(), false);
135           else
136             RenderItem(pos, origin.y + col * m_layout->Size(VERTICAL), item.get(), false);
137         }
138       }
139       // increment our position
140       if (col < m_itemsPerRow - 1)
141         col++;
142       else
143       {
144         pos += m_layout->Size(m_orientation);
145         col = 0;
146       }
147       current++;
148     }
149     // and render the focused item last (for overlapping purposes)
150     if (focusedItem)
151     {
152       if (m_orientation == VERTICAL)
153         RenderItem(origin.x + focusedCol * m_layout->Size(HORIZONTAL), focusedPos, focusedItem.get(), true);
154       else
155         RenderItem(focusedPos, origin.y + focusedCol * m_layout->Size(VERTICAL), focusedItem.get(), true);
156     }
157 
158     CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion();
159   }
160   CGUIControl::Render();
161 }
162 
OnAction(const CAction & action)163 bool CGUIPanelContainer::OnAction(const CAction &action)
164 {
165   switch (action.GetID())
166   {
167   case ACTION_PAGE_UP:
168     {
169       if (GetOffset() == 0)
170       { // already on the first page, so move to the first item
171         SetCursor(0);
172       }
173       else
174       { // scroll up to the previous page
175         Scroll( -m_itemsPerPage);
176       }
177       return true;
178     }
179     break;
180   case ACTION_PAGE_DOWN:
181     {
182       if ((GetOffset() + m_itemsPerPage) * m_itemsPerRow >= (int)m_items.size() || (int)m_items.size() < m_itemsPerPage)
183       { // already at the last page, so move to the last item.
184         SetCursor(m_items.size() - GetOffset() * m_itemsPerRow - 1);
185       }
186       else
187       { // scroll down to the next page
188         Scroll(m_itemsPerPage);
189       }
190       return true;
191     }
192     break;
193     // smooth scrolling (for analog controls)
194   case ACTION_SCROLL_UP:
195     {
196       m_analogScrollCount += action.GetAmount() * action.GetAmount();
197       bool handled = false;
198       while (m_analogScrollCount > AnalogScrollSpeed())
199       {
200         handled = true;
201         m_analogScrollCount -= AnalogScrollSpeed();
202         if (GetOffset() > 0)// && GetCursor() <= m_itemsPerPage * m_itemsPerRow / 2)
203         {
204           Scroll(-1);
205         }
206         else if (GetCursor() > 0)
207         {
208           SetCursor(GetCursor() - 1);
209         }
210       }
211       return handled;
212     }
213     break;
214   case ACTION_SCROLL_DOWN:
215     {
216       m_analogScrollCount += action.GetAmount() * action.GetAmount();
217       bool handled = false;
218       while (m_analogScrollCount > AnalogScrollSpeed())
219       {
220         handled = true;
221         m_analogScrollCount -= AnalogScrollSpeed();
222         if ((GetOffset() + m_itemsPerPage) * m_itemsPerRow < (int)m_items.size())// && GetCursor() >= m_itemsPerPage * m_itemsPerRow / 2)
223         {
224           Scroll(1);
225         }
226         else if (GetCursor() < m_itemsPerPage * m_itemsPerRow - 1 && GetOffset() * m_itemsPerRow + GetCursor() < (int)m_items.size() - 1)
227         {
228           SetCursor(GetCursor() + 1);
229         }
230       }
231       return handled;
232     }
233     break;
234   }
235   return CGUIBaseContainer::OnAction(action);
236 }
237 
OnMessage(CGUIMessage & message)238 bool CGUIPanelContainer::OnMessage(CGUIMessage& message)
239 {
240   if (message.GetControlId() == GetID() )
241   {
242     if (message.GetMessage() == GUI_MSG_LABEL_RESET)
243     {
244       SetCursor(0);
245       // fall through to base class
246     }
247   }
248   return CGUIBaseContainer::OnMessage(message);
249 }
250 
OnLeft()251 void CGUIPanelContainer::OnLeft()
252 {
253   CGUIAction action = GetAction(ACTION_MOVE_LEFT);
254   bool wrapAround = action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition();
255   if (m_orientation == VERTICAL && MoveLeft(wrapAround))
256     return;
257   if (m_orientation == HORIZONTAL && MoveUp(wrapAround))
258     return;
259   CGUIControl::OnLeft();
260 }
261 
OnRight()262 void CGUIPanelContainer::OnRight()
263 {
264   CGUIAction action = GetAction(ACTION_MOVE_RIGHT);
265   bool wrapAround = action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition();
266   if (m_orientation == VERTICAL && MoveRight(wrapAround))
267     return;
268   if (m_orientation == HORIZONTAL && MoveDown(wrapAround))
269     return;
270   return CGUIControl::OnRight();
271 }
272 
OnUp()273 void CGUIPanelContainer::OnUp()
274 {
275   CGUIAction action = GetAction(ACTION_MOVE_UP);
276   bool wrapAround = action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition();
277   if (m_orientation == VERTICAL && MoveUp(wrapAround))
278     return;
279   if (m_orientation == HORIZONTAL && MoveLeft(wrapAround))
280     return;
281   CGUIControl::OnUp();
282 }
283 
OnDown()284 void CGUIPanelContainer::OnDown()
285 {
286   CGUIAction action = GetAction(ACTION_MOVE_DOWN);
287   bool wrapAround = action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition();
288   if (m_orientation == VERTICAL && MoveDown(wrapAround))
289     return;
290   if (m_orientation == HORIZONTAL && MoveRight(wrapAround))
291     return;
292   return CGUIControl::OnDown();
293 }
294 
MoveDown(bool wrapAround)295 bool CGUIPanelContainer::MoveDown(bool wrapAround)
296 {
297   if (GetCursor() + m_itemsPerRow < m_itemsPerPage * m_itemsPerRow && (GetOffset() + 1 + GetCursor() / m_itemsPerRow) * m_itemsPerRow < (int)m_items.size())
298   { // move to last item if necessary
299     if ((GetOffset() + 1)*m_itemsPerRow + GetCursor() >= (int)m_items.size())
300       SetCursor((int)m_items.size() - 1 - GetOffset()*m_itemsPerRow);
301     else
302       SetCursor(GetCursor() + m_itemsPerRow);
303   }
304   else if ((GetOffset() + 1 + GetCursor() / m_itemsPerRow) * m_itemsPerRow < (int)m_items.size())
305   { // we scroll to the next row, and move to last item if necessary
306     if ((GetOffset() + 1)*m_itemsPerRow + GetCursor() >= (int)m_items.size())
307       SetCursor((int)m_items.size() - 1 - (GetOffset() + 1)*m_itemsPerRow);
308     ScrollToOffset(GetOffset() + 1);
309   }
310   else if (wrapAround)
311   { // move first item in list
312     SetCursor(GetCursor() % m_itemsPerRow);
313     ScrollToOffset(0);
314     SetContainerMoving(1);
315   }
316   else
317     return false;
318   return true;
319 }
320 
MoveUp(bool wrapAround)321 bool CGUIPanelContainer::MoveUp(bool wrapAround)
322 {
323   if (GetCursor() >= m_itemsPerRow)
324     SetCursor(GetCursor() - m_itemsPerRow);
325   else if (GetOffset() > 0)
326     ScrollToOffset(GetOffset() - 1);
327   else if (wrapAround)
328   { // move last item in list in this column
329     SetCursor((GetCursor() % m_itemsPerRow) + (m_itemsPerPage - 1) * m_itemsPerRow);
330     int offset = std::max((int)GetRows() - m_itemsPerPage, 0);
331     // should check here whether cursor is actually allowed here, and reduce accordingly
332     if (offset * m_itemsPerRow + GetCursor() >= (int)m_items.size())
333       SetCursor((int)m_items.size() - offset * m_itemsPerRow - 1);
334     ScrollToOffset(offset);
335     SetContainerMoving(-1);
336   }
337   else
338     return false;
339   return true;
340 }
341 
MoveLeft(bool wrapAround)342 bool CGUIPanelContainer::MoveLeft(bool wrapAround)
343 {
344   int col = GetCursor() % m_itemsPerRow;
345   if (col > 0)
346     SetCursor(GetCursor() - 1);
347   else if (wrapAround)
348   { // wrap around
349     SetCursor(GetCursor() + m_itemsPerRow - 1);
350     if (GetOffset() * m_itemsPerRow + GetCursor() >= (int)m_items.size())
351       SetCursor((int)m_items.size() - GetOffset() * m_itemsPerRow - 1);
352   }
353   else
354     return false;
355   return true;
356 }
357 
MoveRight(bool wrapAround)358 bool CGUIPanelContainer::MoveRight(bool wrapAround)
359 {
360   int col = GetCursor() % m_itemsPerRow;
361   if (col + 1 < m_itemsPerRow && GetOffset() * m_itemsPerRow + GetCursor() + 1 < (int)m_items.size())
362     SetCursor(GetCursor() + 1);
363   else if (wrapAround) // move first item in row
364     SetCursor(GetCursor() - col);
365   else
366     return false;
367   return true;
368 }
369 
370 // scrolls the said amount
Scroll(int amount)371 void CGUIPanelContainer::Scroll(int amount)
372 {
373   // increase or decrease the offset
374   int offset = GetOffset() + amount;
375   if (offset > ((int)GetRows() - m_itemsPerPage) * m_itemsPerRow)
376   {
377     offset = ((int)GetRows() - m_itemsPerPage) * m_itemsPerRow;
378   }
379   if (offset < 0) offset = 0;
380   ScrollToOffset(offset);
381 }
382 
ValidateOffset()383 void CGUIPanelContainer::ValidateOffset()
384 {
385   if (!m_layout) return;
386   // first thing is we check the range of our offset
387   // don't validate offset if we are scrolling in case the tween image exceed <0, 1> range
388   if (GetOffset() > (int)GetRows() - m_itemsPerPage || (!m_scroller.IsScrolling() && m_scroller.GetValue() > ((int)GetRows() - m_itemsPerPage) * m_layout->Size(m_orientation)))
389   {
390     SetOffset(std::max(0, (int)GetRows() - m_itemsPerPage));
391     m_scroller.SetValue(GetOffset() * m_layout->Size(m_orientation));
392   }
393   if (GetOffset() < 0 || (!m_scroller.IsScrolling() && m_scroller.GetValue() < 0))
394   {
395     SetOffset(0);
396     m_scroller.SetValue(0);
397   }
398 }
399 
SetCursor(int cursor)400 void CGUIPanelContainer::SetCursor(int cursor)
401 {
402   if (cursor > m_itemsPerPage * m_itemsPerRow - 1)
403     cursor = m_itemsPerPage * m_itemsPerRow - 1;
404   if (cursor < 0) cursor = 0;
405   if (!m_wasReset)
406     SetContainerMoving(cursor - GetCursor());
407   CGUIBaseContainer::SetCursor(cursor);
408 }
409 
CalculateLayout()410 void CGUIPanelContainer::CalculateLayout()
411 {
412   GetCurrentLayouts();
413 
414   if (!m_layout || !m_focusedLayout) return;
415   // calculate the number of items to display
416   if (m_orientation == HORIZONTAL)
417   {
418     m_itemsPerRow = (int)(m_height / m_layout->Size(VERTICAL));
419     m_itemsPerPage = (int)(m_width / m_layout->Size(HORIZONTAL));
420   }
421   else
422   {
423     m_itemsPerRow = (int)(m_width / m_layout->Size(HORIZONTAL));
424     m_itemsPerPage = (int)(m_height / m_layout->Size(VERTICAL));
425   }
426   if (m_itemsPerRow < 1) m_itemsPerRow = 1;
427   if (m_itemsPerPage < 1) m_itemsPerPage = 1;
428 
429   // ensure that the scroll offset is a multiple of our size
430   m_scroller.SetValue(GetOffset() * m_layout->Size(m_orientation));
431 }
432 
GetRows() const433 unsigned int CGUIPanelContainer::GetRows() const
434 {
435   assert(m_itemsPerRow > 0);
436   return (m_items.size() + m_itemsPerRow - 1) / m_itemsPerRow;
437 }
438 
AnalogScrollSpeed() const439 float CGUIPanelContainer::AnalogScrollSpeed() const
440 {
441   return 10.0f / m_itemsPerPage;
442 }
443 
CorrectOffset(int offset,int cursor) const444 int CGUIPanelContainer::CorrectOffset(int offset, int cursor) const
445 {
446   return offset * m_itemsPerRow + cursor;
447 }
448 
GetCursorFromPoint(const CPoint & point,CPoint * itemPoint) const449 int CGUIPanelContainer::GetCursorFromPoint(const CPoint &point, CPoint *itemPoint) const
450 {
451   if (!m_layout)
452     return -1;
453 
454   float sizeX = m_orientation == VERTICAL ? m_layout->Size(HORIZONTAL) : m_layout->Size(VERTICAL);
455   float sizeY = m_orientation == VERTICAL ? m_layout->Size(VERTICAL) : m_layout->Size(HORIZONTAL);
456 
457   float posY = m_orientation == VERTICAL ? point.y : point.x;
458   for (int y = 0; y < m_itemsPerPage + 1; y++) // +1 to ensure if we have a half item we can select it
459   {
460     float posX = m_orientation == VERTICAL ? point.x : point.y;
461     for (int x = 0; x < m_itemsPerRow; x++)
462     {
463       int item = x + y * m_itemsPerRow;
464       if (posX < sizeX && posY < sizeY && item + GetOffset() < (int)m_items.size())
465       { // found
466         return item;
467       }
468       posX -= sizeX;
469     }
470     posY -= sizeY;
471   }
472   return -1;
473 }
474 
SelectItemFromPoint(const CPoint & point)475 bool CGUIPanelContainer::SelectItemFromPoint(const CPoint &point)
476 {
477   int cursor = GetCursorFromPoint(point);
478   if (cursor < 0)
479     return false;
480   SetCursor(cursor);
481   return true;
482 }
483 
GetCurrentRow() const484 int CGUIPanelContainer::GetCurrentRow() const
485 {
486   return m_itemsPerRow > 0 ? GetCursor() / m_itemsPerRow : 0;
487 }
488 
GetCurrentColumn() const489 int CGUIPanelContainer::GetCurrentColumn() const
490 {
491   return GetCursor() % m_itemsPerRow;
492 }
493 
GetCondition(int condition,int data) const494 bool CGUIPanelContainer::GetCondition(int condition, int data) const
495 {
496   int row = GetCurrentRow();
497   int col = GetCurrentColumn();
498 
499   if (m_orientation == HORIZONTAL)
500     std::swap(row, col);
501 
502   switch (condition)
503   {
504   case CONTAINER_ROW:
505     return (row == data);
506   case CONTAINER_COLUMN:
507     return (col == data);
508   default:
509     return CGUIBaseContainer::GetCondition(condition, data);
510   }
511 }
512 
GetLabel(int info) const513 std::string CGUIPanelContainer::GetLabel(int info) const
514 {
515   int row = GetCurrentRow();
516   int col = GetCurrentColumn();
517 
518   if (m_orientation == HORIZONTAL)
519     std::swap(row, col);
520 
521   switch (info)
522   {
523   case CONTAINER_ROW:
524     return StringUtils::Format("%i", row);
525   case CONTAINER_COLUMN:
526     return StringUtils::Format("%i", col);
527   default:
528     return CGUIBaseContainer::GetLabel(info);
529   }
530   return StringUtils::Empty;
531 }
532 
SelectItem(int item)533 void CGUIPanelContainer::SelectItem(int item)
534 {
535   // Check that our offset is valid
536   ValidateOffset();
537   // only select an item if it's in a valid range
538   if (item >= 0 && item < (int)m_items.size())
539   {
540     // Select the item requested
541     if (item >= GetOffset() * m_itemsPerRow && item < (GetOffset() + m_itemsPerPage) * m_itemsPerRow)
542     { // the item is on the current page, so don't change it.
543       SetCursor(item - GetOffset() * m_itemsPerRow);
544     }
545     else if (item < GetOffset() * m_itemsPerRow)
546     { // item is on a previous page - make it the first item on the page
547       SetCursor(item % m_itemsPerRow);
548       ScrollToOffset((item - GetCursor()) / m_itemsPerRow);
549     }
550     else // (item >= GetOffset()+m_itemsPerPage)
551     { // item is on a later page - make it the last row on the page
552       SetCursor(item % m_itemsPerRow + m_itemsPerRow * (m_itemsPerPage - 1));
553       ScrollToOffset((item - GetCursor()) / m_itemsPerRow);
554     }
555   }
556 }
557 
HasPreviousPage() const558 bool CGUIPanelContainer::HasPreviousPage() const
559 {
560   return (GetOffset() > 0);
561 }
562 
HasNextPage() const563 bool CGUIPanelContainer::HasNextPage() const
564 {
565   return (GetOffset() != (int)GetRows() - m_itemsPerPage && (int)GetRows() > m_itemsPerPage);
566 }
567 
568