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