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 "GUIFixedListContainer.h"
10 
11 #include "GUIListItemLayout.h"
12 #include "input/Key.h"
13 
CGUIFixedListContainer(int parentID,int controlID,float posX,float posY,float width,float height,ORIENTATION orientation,const CScroller & scroller,int preloadItems,int fixedPosition,int cursorRange)14 CGUIFixedListContainer::CGUIFixedListContainer(int parentID, int controlID, float posX, float posY, float width, float height, ORIENTATION orientation, const CScroller& scroller, int preloadItems, int fixedPosition, int cursorRange)
15     : CGUIBaseContainer(parentID, controlID, posX, posY, width, height, orientation, scroller, preloadItems)
16 {
17   ControlType = GUICONTAINER_FIXEDLIST;
18   m_type = VIEW_TYPE_LIST;
19   m_fixedCursor = fixedPosition;
20   m_cursorRange = std::max(0, cursorRange);
21   SetCursor(m_fixedCursor);
22 }
23 
24 CGUIFixedListContainer::~CGUIFixedListContainer(void) = default;
25 
OnAction(const CAction & action)26 bool CGUIFixedListContainer::OnAction(const CAction &action)
27 {
28   switch (action.GetID())
29   {
30   case ACTION_PAGE_UP:
31     {
32       Scroll(-m_itemsPerPage);
33       return true;
34     }
35     break;
36   case ACTION_PAGE_DOWN:
37     {
38       Scroll(m_itemsPerPage);
39       return true;
40     }
41     break;
42     // smooth scrolling (for analog controls)
43   case ACTION_SCROLL_UP:
44     {
45       m_analogScrollCount += action.GetAmount() * action.GetAmount();
46       bool handled = false;
47       while (m_analogScrollCount > 0.4)
48       {
49         handled = true;
50         m_analogScrollCount -= 0.4f;
51         Scroll(-1);
52       }
53       return handled;
54     }
55     break;
56   case ACTION_SCROLL_DOWN:
57     {
58       m_analogScrollCount += action.GetAmount() * action.GetAmount();
59       bool handled = false;
60       while (m_analogScrollCount > 0.4)
61       {
62         handled = true;
63         m_analogScrollCount -= 0.4f;
64         Scroll(1);
65       }
66       return handled;
67     }
68     break;
69   }
70   return CGUIBaseContainer::OnAction(action);
71 }
72 
MoveUp(bool wrapAround)73 bool CGUIFixedListContainer::MoveUp(bool wrapAround)
74 {
75   int item = GetSelectedItem();
76   if (item > 0)
77     SelectItem(item - 1);
78   else if (wrapAround)
79   {
80     SelectItem((int)m_items.size() - 1);
81     SetContainerMoving(-1);
82   }
83   else
84     return false;
85   return true;
86 }
87 
MoveDown(bool wrapAround)88 bool CGUIFixedListContainer::MoveDown(bool wrapAround)
89 {
90   int item = GetSelectedItem();
91   if (item < (int)m_items.size() - 1)
92     SelectItem(item + 1);
93   else if (wrapAround)
94   { // move first item in list
95     SelectItem(0);
96     SetContainerMoving(1);
97   }
98   else
99     return false;
100   return true;
101 }
102 
Scroll(int amount)103 void CGUIFixedListContainer::Scroll(int amount)
104 {
105   // increase or decrease the offset within [-minCursor, m_items.size() - maxCursor]
106   int minCursor, maxCursor;
107   GetCursorRange(minCursor, maxCursor);
108   const int nextCursor = GetCursor() + amount;
109   int offset = GetOffset() + amount;
110   if (offset < -minCursor)
111   {
112     offset = -minCursor;
113     SetCursor(nextCursor < minCursor ? minCursor : nextCursor);
114   }
115   if (offset > (int)m_items.size() - 1 - maxCursor)
116   {
117     offset = m_items.size() - 1 - maxCursor;
118     SetCursor(nextCursor > maxCursor ? maxCursor : nextCursor);
119   }
120   ScrollToOffset(offset);
121 }
122 
GetOffsetRange(int & minOffset,int & maxOffset) const123 bool CGUIFixedListContainer::GetOffsetRange(int &minOffset, int &maxOffset) const
124 {
125   GetCursorRange(minOffset, maxOffset);
126   minOffset = -minOffset;
127   maxOffset = m_items.size() - maxOffset - 1;
128   return true;
129 }
130 
ValidateOffset()131 void CGUIFixedListContainer::ValidateOffset()
132 {
133   if (!m_layout) return;
134   // ensure our fixed cursor position is valid
135   if (m_fixedCursor >= m_itemsPerPage)
136     m_fixedCursor = m_itemsPerPage - 1;
137   if (m_fixedCursor < 0)
138     m_fixedCursor = 0;
139   // compute our minimum and maximum cursor positions
140   int minCursor, maxCursor;
141   GetCursorRange(minCursor, maxCursor);
142   // assure our cursor is between these limits
143   SetCursor(std::max(GetCursor(), minCursor));
144   SetCursor(std::min(GetCursor(), maxCursor));
145   int minOffset, maxOffset;
146   GetOffsetRange(minOffset, maxOffset);
147   // and finally ensure our offset is valid
148   // don't validate offset if we are scrolling in case the tween image exceed <0, 1> range
149   if (GetOffset() > maxOffset || (!m_scroller.IsScrolling() && m_scroller.GetValue() > maxOffset * m_layout->Size(m_orientation)))
150   {
151     SetOffset(std::max(-minCursor, maxOffset));
152     m_scroller.SetValue(GetOffset() * m_layout->Size(m_orientation));
153   }
154   if (GetOffset() < minOffset || (!m_scroller.IsScrolling() && m_scroller.GetValue() < minOffset * m_layout->Size(m_orientation)))
155   {
156     SetOffset(minOffset);
157     m_scroller.SetValue(GetOffset() * m_layout->Size(m_orientation));
158   }
159 }
160 
GetCursorFromPoint(const CPoint & point,CPoint * itemPoint) const161 int CGUIFixedListContainer::GetCursorFromPoint(const CPoint &point, CPoint *itemPoint) const
162 {
163   if (!m_focusedLayout || !m_layout)
164     return -1;
165   int minCursor, maxCursor;
166   GetCursorRange(minCursor, maxCursor);
167   // see if the point is either side of our focus range
168   float start = (minCursor + 0.2f) * m_layout->Size(m_orientation);
169   float end = (maxCursor - 0.2f) * m_layout->Size(m_orientation) + m_focusedLayout->Size(m_orientation);
170   float pos = (m_orientation == VERTICAL) ? point.y : point.x;
171   if (pos >= start && pos <= end)
172   { // select the appropriate item
173     pos -= minCursor * m_layout->Size(m_orientation);
174     for (int row = minCursor; row <= maxCursor; row++)
175     {
176       const CGUIListItemLayout *layout = (row == GetCursor()) ? m_focusedLayout : m_layout;
177       if (pos < layout->Size(m_orientation))
178       {
179         if (!InsideLayout(layout, point))
180           return -1;
181         return row;
182       }
183       pos -= layout->Size(m_orientation);
184     }
185   }
186   return -1;
187 }
188 
SelectItemFromPoint(const CPoint & point)189 bool CGUIFixedListContainer::SelectItemFromPoint(const CPoint &point)
190 {
191   if (!m_focusedLayout || !m_layout)
192     return false;
193 
194   MarkDirtyRegion();
195 
196   const float mouse_scroll_speed = 0.25f;
197   const float mouse_max_amount = 1.5f;
198   float sizeOfItem = m_layout->Size(m_orientation);
199   int minCursor, maxCursor;
200   GetCursorRange(minCursor, maxCursor);
201   // see if the point is either side of our focus range
202   float start = (minCursor + 0.2f) * sizeOfItem;
203   float end = (maxCursor - 0.2f) * sizeOfItem + m_focusedLayout->Size(m_orientation);
204   float pos = (m_orientation == VERTICAL) ? point.y : point.x;
205   if (pos < start && GetOffset() > -minCursor)
206   { // scroll backward
207     if (!InsideLayout(m_layout, point))
208       return false;
209     float amount = std::min((start - pos) / sizeOfItem, mouse_max_amount);
210     m_analogScrollCount += amount * amount * mouse_scroll_speed;
211     if (m_analogScrollCount > 1)
212     {
213       ScrollToOffset(GetOffset() - 1);
214       m_analogScrollCount = 0;
215     }
216     return true;
217   }
218   else if (pos > end && GetOffset() + maxCursor < (int)m_items.size() - 1)
219   {
220     if (!InsideLayout(m_layout, point))
221       return false;
222     // scroll forward
223     float amount = std::min((pos - end) / sizeOfItem, mouse_max_amount);
224     m_analogScrollCount += amount * amount * mouse_scroll_speed;
225     if (m_analogScrollCount > 1)
226     {
227       ScrollToOffset(GetOffset() + 1);
228       m_analogScrollCount = 0;
229     }
230     return true;
231   }
232   else
233   { // select the appropriate item
234     int cursor = GetCursorFromPoint(point);
235     if (cursor < 0)
236       return false;
237     // calling SelectItem() here will focus the item and scroll, which isn't really what we're after
238     SetCursor(cursor);
239     return true;
240   }
241 }
242 
SelectItem(int item)243 void CGUIFixedListContainer::SelectItem(int item)
244 {
245   // Check that GetOffset() is valid
246   ValidateOffset();
247   // only select an item if it's in a valid range
248   if (item >= 0 && item < (int)m_items.size())
249   {
250     // Select the item requested - we first set the cursor position
251     // which may be different at either end of the list, then the offset
252     int minCursor, maxCursor;
253     GetCursorRange(minCursor, maxCursor);
254 
255     int cursor;
256     if ((int)m_items.size() - 1 - item <= maxCursor - m_fixedCursor)
257       cursor = std::max(m_fixedCursor, maxCursor + item - (int)m_items.size() + 1);
258     else if (item <= m_fixedCursor - minCursor)
259       cursor = std::min(m_fixedCursor, minCursor + item);
260     else
261       cursor = m_fixedCursor;
262     if (cursor != GetCursor())
263       SetContainerMoving(cursor - GetCursor());
264     SetCursor(cursor);
265     ScrollToOffset(item - GetCursor());
266     MarkDirtyRegion();
267   }
268 }
269 
HasPreviousPage() const270 bool CGUIFixedListContainer::HasPreviousPage() const
271 {
272   return (GetOffset() > 0);
273 }
274 
HasNextPage() const275 bool CGUIFixedListContainer::HasNextPage() const
276 {
277   return (GetOffset() < (int)m_items.size() - m_itemsPerPage && (int)m_items.size() >= m_itemsPerPage);
278 }
279 
GetCurrentPage() const280 int CGUIFixedListContainer::GetCurrentPage() const
281 {
282   int offset = CorrectOffset(GetOffset(), GetCursor());
283   if (offset + m_itemsPerPage - GetCursor() >= (int)GetRows())  // last page
284     return (GetRows() + m_itemsPerPage - 1) / m_itemsPerPage;
285   return offset / m_itemsPerPage + 1;
286 }
287 
GetCursorRange(int & minCursor,int & maxCursor) const288 void CGUIFixedListContainer::GetCursorRange(int &minCursor, int &maxCursor) const
289 {
290   minCursor = std::max(m_fixedCursor - m_cursorRange, 0);
291   maxCursor = std::min(m_fixedCursor + m_cursorRange, m_itemsPerPage);
292 
293   if (!m_items.size())
294   {
295     minCursor = m_fixedCursor;
296     maxCursor = m_fixedCursor;
297     return;
298   }
299 
300   while (maxCursor - minCursor > (int)m_items.size() - 1)
301   {
302     if (maxCursor - m_fixedCursor > m_fixedCursor - minCursor)
303       maxCursor--;
304     else
305       minCursor++;
306   }
307 }
308 
309