MythTV master
mythuibuttonlist.cpp
Go to the documentation of this file.
1#include "mythuibuttonlist.h"
2
3#include <cmath>
4#include <algorithm>
5#include <utility>
6
7// QT headers
8#include <QCoreApplication>
9#include <QDomDocument>
10#include <QKeyEvent>
11#include <QRegularExpression>
12
13// libmythbase headers
16
17// mythui headers
18#include "mythmainwindow.h"
19#include "mythuiscrollbar.h"
20#include "mythuistatetype.h"
21#include "mythuibutton.h"
22#include "mythuitext.h"
23#include "mythuitextedit.h"
24#include "mythuigroup.h"
25#include "mythuiimage.h"
26#include "mythgesture.h"
27#include "mythuiprogressbar.h"
28
29#define LOC QString("MythUIButtonList(%1): ").arg(objectName())
30
32 QString shadow)
33 : MythUIType(parent, name)
34 , m_shadowListName(std::move(shadow))
35{
36 // Parent members
39
40 Const();
41}
42
44 const QRect area, bool showArrow,
45 bool showScrollBar)
46 : MythUIType(parent, name),
47 m_showArrow(showArrow), m_showScrollBar(showScrollBar)
48{
49 // Parent members
50 m_area = area;
51 m_initiator = true;
52 m_enableInitiator = true;
53
56
57 Const();
58}
59
61{
62 SetCanTakeFocus(true);
63
67}
68
70{
71 m_buttonToItem.clear();
72 m_clearing = true;
73
74 while (!m_itemList.isEmpty())
75 delete m_itemList.takeFirst();
76}
77
79{
81
82 if (item)
83 emit itemSelected(item);
84
85 SetActive(true);
86}
87
89{
90 SetActive(false);
91}
92
94{
95 if (m_initialized)
96 Update();
97}
98
99#if 0
100void MythUIButtonList::SetDrawFromBottom(bool draw)
101{
102 m_drawFromBottom = draw;
103}
104#endif
105
107{
108 if (m_active == active)
109 return;
110
111 m_active = active;
112
113 if (m_initialized)
114 Update();
115}
116
121{
122 m_buttonToItem.clear();
123
124 if (m_itemList.isEmpty())
125 return;
126
127 m_clearing = true;
128
129 while (!m_itemList.isEmpty())
130 delete m_itemList.takeFirst();
131
132 m_clearing = false;
133
134 m_selPosition = 0;
135 m_topPosition = 0;
136 m_itemCount = 0;
137
138 StopLoad();
139 Update();
141
142 emit DependChanged(true);
143}
144
146{
147 m_needsUpdate = true;
148 SetRedraw();
149}
150
151/*
152 * The "width" of a button determines it relative position when using
153 * Dynamic-Layout.
154 *
155 * If a button has a negative offset, that offset needs accounted for
156 * to position the button in proper releation to the surrounding buttons.
157 */
159{
160 int width = area.width();
161
162 if (area.x() < 0)
163 {
164 /*
165 * Assume if an overlap is allowed on the left, the same overlap
166 * is on the right
167 */
168 width += ((area.x() * 2) - 1); // x is negative
169
170 while (width < 0)
171 width -= area.x(); // Oops
172 }
173 else if (m_layout == LayoutHorizontal)
174 {
175 width -= area.x(); // Get rid of any "space" betwen the buttons
176 }
177
178 return width;
179}
180
181/*
182 * The "height" of a button determines it relative position when using
183 * Dynamic-Layout.
184 *
185 * If a button has a negative offset, that offset needs accounted for
186 * to position the button in proper releation to the surrounding buttons.
187 */
189{
190 int height = area.height();
191
192 if (area.y() < 0)
193 {
194 /*
195 * Assume if an overlap is allowed on the top, the same overlap
196 * is on the bottom
197 */
198 height += ((area.y() * 2) - 1);
199
200 while (height < 0)
201 height -= area.y(); // Oops
202 }
203 else if (m_layout == LayoutVertical)
204 {
205 height -= area.y(); // Get rid of any "space" betwen the buttons
206 }
207
208 return height;
209}
210
211/*
212 * For Dynamic-Layout, buttons are allocated as needed. If the list is
213 * being redrawn, re-use any previously allocated buttons.
214 */
216 int &selectedIdx,
217 int &button_shift)
218{
219 MythUIButtonListItem *buttonItem = m_itemList[itemIdx];
220
221 buttonIdx += button_shift;
222
223 if (buttonIdx < 0 || buttonIdx + 1 > m_maxVisible)
224 {
225 QString name = QString("buttonlist button %1").arg(m_maxVisible);
226 auto *button = new MythUIStateType(this, name);
227 button->CopyFrom(m_buttontemplate);
228 button->ConnectDependants(true);
229
230 if (buttonIdx < 0)
231 {
232 /*
233 * If a new button is needed in the front of the list, previously
234 * existing buttons need shifted to the right.
235 */
236 m_buttonList.prepend(button);
237 buttonIdx = 0;
238 ++button_shift;
239
240 if (selectedIdx >= 0)
241 ++selectedIdx;
242 }
243 else
244 {
245 m_buttonList.append(button);
246 }
247
248 ++m_maxVisible;
249 }
250
251 MythUIStateType *realButton = m_buttonList[buttonIdx];
252 m_buttonToItem[buttonIdx] = buttonItem;
253 buttonItem->SetToRealButton(realButton, itemIdx == m_selPosition);
254 auto *buttonstate =
255 dynamic_cast<MythUIGroup *>(realButton->GetCurrentState());
256
257 if (itemIdx == m_selPosition)
258 selectedIdx = buttonIdx;
259
260 return buttonstate;
261}
262
263/*
264 * Dynamically layout the buttons on a row.
265 */
266bool MythUIButtonList::DistributeRow(int &first_button, int &last_button,
267 int &first_item, int &last_item,
268 int &selected_column, int &skip_cols,
269 bool grow_left, bool grow_right,
270 int **col_widths, int &row_height,
271 int total_height, int split_height,
272 int &col_cnt, bool &wrapped)
273{
274 MythUIGroup *buttonstate = nullptr;
275 int left_width = 0;
276 int right_width = 0;
277 int begin = 0;
278 int end = 0;
279 bool underflow = false;
280
281 int selectedIdx = -1;
282 int button_shift = 0;
283 col_cnt = 1;
284 skip_cols = 0;
285
286 if (last_item + 1 > m_itemCount || last_item < 0 || first_item < 0)
287 return false;
288
289 /*
290 * Allocate a button on the row. With a vertical layout, there is
291 * only one button per row, and this would be it.
292 */
293 if (grow_right)
294 {
295 buttonstate = PrepareButton(last_button, last_item,
296 selectedIdx, button_shift);
297 }
298 else
299 {
300 buttonstate = PrepareButton(first_button, first_item,
301 selectedIdx, button_shift);
302 }
303
304 if (buttonstate == nullptr)
305 {
306 LOG(VB_GENERAL, LOG_ERR, QString("Failed to query buttonlist state: %1")
307 .arg(last_button));
308 return false;
309 }
310
311 // Note size of initial button.
312 int max_width = m_contentsRect.width();
313 int max_height = m_contentsRect.height();
314 row_height = minButtonHeight(buttonstate->GetArea());
315 int width = minButtonWidth(buttonstate->GetArea());
316
317 /*
318 * If the selected button should be centered, don't allow new buttons
319 * to take up more than half the allowed area.
320 */
321 bool vsplit = (m_scrollStyle == ScrollCenter);
322 bool hsplit = vsplit && grow_left && grow_right;
323
324 if (hsplit)
325 {
326 max_width /= 2;
327 left_width = right_width = (width / 2);
328 }
329 else
330 {
331 if (grow_right)
332 {
333 left_width = 0;
334 right_width = width;
335 }
336 else
337 {
338 left_width = width;
339 right_width = 0;
340 }
341 }
342
343 if (vsplit)
344 max_height /= 2;
345
346 /*
347 * If total_height == 0, then this is the first row, so allow any height.
348 * Otherwise, If adding a button to a column would exceed the
349 * parent height, abort
350 */
351 if (total_height > 0 &&
352 ((vsplit ? split_height : total_height) +
353 m_itemVertSpacing + row_height > max_height))
354 {
355 LOG(VB_GUI, LOG_DEBUG,
356 QString("%1 Height exceeded %2 + (%3) + %4 = %5 which is > %6")
357 .arg(vsplit ? "Centering" : "Total")
358 .arg(split_height).arg(m_itemVertSpacing).arg(row_height)
359 .arg(split_height + m_itemVertSpacing + row_height)
360 .arg(max_height));
361 first_button += button_shift;
362 last_button += button_shift;
363 return false;
364 }
365
366 LOG(VB_GUI, LOG_DEBUG, QString("Added button item %1 width %2 height %3")
367 .arg(grow_right ? last_item : first_item)
368 .arg(width).arg(row_height));
369
370 int initial_first_button = first_button;
371 int initial_last_button = last_button;
372 int initial_first_item = first_item;
373 int initial_last_item = last_item;
374
375 /*
376 * if col_widths is not nullptr, then grow_left & grow_right
377 * are mutually exclusive. So, col_idx can be anchored from
378 * the left or right.
379 */
380 int col_idx = 0;
381 if (!grow_right)
382 col_idx = m_columns - 1;
383
384 // Add butons until no more fit.
385 bool added = (m_layout != LayoutVertical);
386
387 while (added)
388 {
389 added = false;
390
391 // If a grid, maintain same number of columns on each row.
392 if (grow_right && col_cnt < m_columns)
393 {
394 if (wrapped)
395 end = first_item;
396 else
397 {
398 // Are we allowed to wrap when we run out of items?
399 if (m_wrapStyle == WrapItems &&
400 (hsplit || m_scrollStyle != ScrollFree) &&
401 last_item + 1 == m_itemCount)
402 {
403 last_item = -1;
404 wrapped = true;
405 end = first_item;
406 }
407 else
408 {
409 end = m_itemCount;
410 }
411 }
412
413 if (last_item + 1 < end)
414 {
415 // Allocate next button to the right.
416 buttonstate = PrepareButton(last_button + 1, last_item + 1,
417 selectedIdx, button_shift);
418
419 if (buttonstate == nullptr)
420 continue;
421
422 width = minButtonWidth(buttonstate->GetArea());
423
424 // For grids, use the widest button in a column
425 if (*col_widths && width < (*col_widths)[col_idx])
426 width = (*col_widths)[col_idx];
427
428 // Does the button fit?
429 if ((hsplit ? right_width : left_width + right_width) +
430 m_itemHorizSpacing + width > max_width)
431 {
432 int total = hsplit ? right_width : left_width + right_width;
433 LOG(VB_GUI, LOG_DEBUG,
434 QString("button on right would exceed width: "
435 "%1+(%2)+%3 == %4 which is > %5")
436 .arg(total).arg(m_itemHorizSpacing).arg(width)
437 .arg(total + m_itemHorizSpacing + width)
438 .arg(max_width));
439 }
440 else
441 {
442 added = true;
443 ++col_cnt;
444 ++last_button;
445 ++last_item;
446 ++col_idx;
447 right_width += m_itemHorizSpacing + width;
448 int height = minButtonHeight(buttonstate->GetArea());
449
450 row_height = std::max(row_height, height);
451
452 LOG(VB_GUI, LOG_DEBUG,
453 QString("Added button item %1 "
454 "R.width %2 height %3 total width %4+%5"
455 " (max %6)")
456 .arg(last_item).arg(width).arg(height)
457 .arg(left_width).arg(right_width).arg(max_width));
458 }
459 }
460 else
461 {
462 underflow = true;
463 }
464 }
465
466 // If a grid, maintain same number of columns on each row.
467 if (grow_left && col_cnt < m_columns)
468 {
469 if (wrapped)
470 end = last_item + 1;
471 else
472 {
473 // Are we allowed to wrap when we run out of items?
474 if (m_wrapStyle == WrapItems &&
475 (hsplit || m_scrollStyle != ScrollFree) &&
476 first_item == 0)
477 {
478 first_item = m_itemCount;
479 wrapped = true;
480 end = last_item + 1;
481 }
482 else
483 {
484 end = 0;
485 }
486 }
487
488 if (first_item > end)
489 {
490 buttonstate = PrepareButton(first_button - 1, first_item - 1,
491 selectedIdx, button_shift);
492
493 if (buttonstate == nullptr)
494 continue;
495
496 width = minButtonWidth(buttonstate->GetArea());
497
498 // For grids, use the widest button in a column
499 if (*col_widths && width < (*col_widths)[col_idx])
500 width = (*col_widths)[col_idx];
501
502 // Does the button fit?
503 if ((hsplit ? left_width : left_width + right_width) +
504 m_itemHorizSpacing + width > max_width)
505 {
506 int total = hsplit ? left_width : left_width + right_width;
507 LOG(VB_GUI, LOG_DEBUG,
508 QString("button on left would exceed width: "
509 "%1+(%2)+%3 == %4 which is > %5")
510 .arg(total).arg(m_itemHorizSpacing).arg(width)
511 .arg(total + m_itemHorizSpacing + width)
512 .arg(max_width));
513 }
514 else
515 {
516 added = true;
517 --first_button;
518 --first_item;
519 --col_idx;
520 ++col_cnt;
521 left_width += m_itemHorizSpacing + width;
522 int height = minButtonHeight(buttonstate->GetArea());
523
524 row_height = std::max(row_height, height);
525
526 LOG(VB_GUI, LOG_DEBUG,
527 QString("Added button item %1 "
528 "L.width %2 height %3 total width %4+%5"
529 " (max %6)")
530 .arg(first_item).arg(width).arg(height)
531 .arg(left_width).arg(right_width).arg(max_width));
532 }
533 }
534 else
535 {
536 underflow = true;
537 if (m_layout == LayoutGrid)
538 skip_cols = m_columns - col_cnt;
539 }
540 }
541 }
542
543 /*
544 * If total_height == 0, then this is the first row, so allow any height.
545 * Otherwise, If adding a button to a column would exceed the
546 * parent height, abort
547 */
548 if (total_height > 0 &&
549 ((vsplit ? split_height : total_height) +
550 m_itemVertSpacing + row_height > max_height))
551 {
552 LOG(VB_GUI, LOG_DEBUG,
553 QString("%1 Height exceeded %2 + (%3) + %4 = %5 which is > %6")
554 .arg(vsplit ? "Centering" : "Total")
555 .arg(split_height).arg(m_itemVertSpacing).arg(row_height)
556 .arg(split_height + m_itemVertSpacing + row_height)
557 .arg(max_height));
558 first_button = initial_first_button + button_shift;
559 last_button = initial_last_button + button_shift;
560 first_item = initial_first_item;
561 last_item = initial_last_item;
562 return false;
563 }
564
565 if (*col_widths == nullptr)
566 {
567 /*
568 * Allocate array to hold columns widths, now that we know
569 * how many columns there are.
570 */
571 *col_widths = new int[static_cast<size_t>(col_cnt)];
572
573 for (col_idx = 0; col_idx < col_cnt; ++col_idx)
574 (*col_widths)[col_idx] = 0;
575
576 }
577
578 // Adjust for insertions on the front.
579 first_button += button_shift;
580 last_button += button_shift;
581
582 // It fits, so so note max column widths
583 MythUIStateType *realButton = nullptr;
584 int buttonIdx = 0;
585
586 if (grow_left)
587 {
588 begin = first_button;
589 end = first_button + col_cnt;
590 }
591 else
592 {
593 end = last_button + 1;
594 begin = end - col_cnt;
595 }
596
597 for (buttonIdx = begin, col_idx = 0;
598 buttonIdx < end; ++buttonIdx, ++col_idx)
599 {
600 realButton = m_buttonList[buttonIdx];
601 buttonstate = dynamic_cast<MythUIGroup *>
602 (realButton->GetCurrentState());
603 if (!buttonstate)
604 break;
605 width = minButtonWidth(buttonstate->GetArea());
606
607 (*col_widths)[col_idx] = std::max((*col_widths)[col_idx], width);
608
609 // Make note of which column has the selected button
610 if (selectedIdx == buttonIdx)
611 selected_column = col_idx;
612 }
613
614 /*
615 An underflow indicates we ran out of items, not that the
616 buttons did not fit on the row.
617 */
618 if (total_height && underflow && col_cnt < m_columns)
619 col_cnt = m_columns;
620
621 return true;
622}
623
624/*
625 * Dynamically layout columns
626 */
627bool MythUIButtonList::DistributeCols(int &first_button, int &last_button,
628 int &first_item, int &last_item,
629 int &selected_column, int &selected_row,
630 int &skip_cols, int **col_widths,
631 QList<int> & row_heights,
632 int &top_height, int &bottom_height,
633 bool &wrapped)
634{
635 int col_cnt = 0;
636 int height = 0;
637 int end = 0;
638 bool added = true;
639
640 while (added)
641 {
642 added = false;
643
644 if (wrapped)
645 end = first_item;
646 else
647 {
648 // Are we allowed to wrap when we run out of items?
649 if (m_wrapStyle == WrapItems &&
652 last_item + 1 == m_itemCount)
653 {
654 last_item = -1;
655 wrapped = true;
656 end = first_item;
657 }
658 else
659 {
660 end = m_itemCount;
661 }
662 }
663
664 if (last_item + 1 < end)
665 {
666 // Does another row fit?
667 if (DistributeRow(first_button, ++last_button,
668 first_item, ++last_item, selected_column,
669 skip_cols, false, true, col_widths, height,
670 top_height + bottom_height, bottom_height,
671 col_cnt, wrapped))
672 {
673 if (col_cnt < m_columns)
674 return false; // Need to try again with fewer cols
675
676 if (selected_row == -1 && selected_column != -1)
677 selected_row = row_heights.size();
678
679 row_heights.push_back(height);
680 bottom_height += (height + m_itemVertSpacing);
681 added = true;
682 }
683 else
684 {
685 --last_button;
686 --last_item;
687 }
688 }
689
690 if (wrapped)
691 end = last_item + 1;
692 else
693 {
694 // Are we allowed to wrap when we run out of items?
695 if (m_wrapStyle == WrapItems &&
698 first_item == 0)
699 {
700 first_item = m_itemCount;
701 wrapped = true;
702 end = last_item + 1;
703 }
704 else
705 {
706 end = 0;
707 }
708 }
709
710 if (first_item > end)
711 {
712 // Can we insert another row?
713 if (DistributeRow(--first_button, last_button,
714 --first_item, last_item, selected_column,
715 skip_cols, true, false, col_widths, height,
716 top_height + bottom_height, top_height,
717 col_cnt, wrapped))
718 {
719 if (col_cnt < m_columns)
720 return false; // Need to try again with fewer cols
721
722 if (selected_row == -1 && selected_column != -1)
723 selected_row = row_heights.size();
724 else if (selected_row != -1)
725 ++selected_row;
726
727 row_heights.push_front(height);
728 top_height += (height + m_itemVertSpacing);
729 added = true;
730 }
731 else
732 {
733 ++first_button;
734 ++first_item;
735 }
736 }
737 }
738
739 return true;
740}
741
742/*
743 * Dynamically layout as many buttons as will fit in the area.
744 */
746{
747 int first_button = 0;
748 int last_button = 0;
749 int start_button = 0;
750 int start_item = m_selPosition;
751 int first_item = 0;
752 int last_item = 0;
753 int skip_cols = 0;
754 int *col_widths = nullptr;
755 int col_cnt = 0;
756 int selected_column = -1;
757 int selected_row = -1;
758 bool wrapped = false;
759 bool grow_left = true;
760 int height = 0;
761 int top_height = 0;
762 int bottom_height = 0;
763
764 QList<int> row_heights;
765
766 int alignment = IsShadowing() && m_shadowAlignment ?
768
769 LOG(VB_GUI, LOG_DEBUG, QString("DistributeButtons: "
770 "selected item %1 total items %2")
771 .arg(start_item).arg(m_itemCount));
772
773 // if there are no items to show make sure all the buttons are made invisible
774 if (m_itemCount == 0)
775 {
776 for (int i = 0; i < m_buttonList.count(); ++i)
777 {
778 if (m_buttonList[i])
779 m_buttonList[i]->SetVisible(false);
780 }
781
782 return false;
783 }
784
785 /*
786 * Try fewer and fewer columns until each row can fit the same
787 * number of columns.
788 */
789 for (m_columns = m_itemCount; m_columns > 0;)
790 {
791 first_item = last_item = start_item;
792
793 /*
794 * Drawing starts at start_button, and radiates from there.
795 * Attempt to pick a start_button which will minimize the need for new
796 * button allocations.
797 */
798 switch (m_scrollStyle)
799 {
800 case ScrollCenter:
802 start_button = std::max((m_maxVisible / 2) - 1, 0);
803 break;
804 case ScrollFree:
805
806 if (m_layout == LayoutGrid)
807 {
808 start_button = 0;
809 first_item = last_item = 0;
810 grow_left = false;
811 }
812 else if (!m_buttonList.empty())
813 {
814 if (m_itemCount - m_selPosition - 1 <
815 (m_buttonList.size() / 2))
816 {
817 start_button = m_buttonList.size() -
819 }
820 else if (m_selPosition >
821 (m_buttonList.size() / 2))
822 {
823 start_button = (m_buttonList.size() / 2);
824 }
825 else
826 {
827 start_button = m_selPosition;
828 }
829 }
830 else
831 {
832 start_button = 0;
833 }
834
835 break;
836 }
837
838 first_button = last_button = start_button;
839 row_heights.clear();
840
841 // Process row with selected button, and set starting val for m_columns.
842 if (!DistributeRow(first_button, last_button,
843 first_item, last_item, selected_column,
844 skip_cols, grow_left, true, &col_widths,
845 height, 0, 0, col_cnt, wrapped))
846 {
847 delete[] col_widths;
848 return false;
849 }
850
851 m_columns = col_cnt;
852
854 {
855 /*
856 * Now that we know how many columns there are, we can start
857 * the grid layout for real.
858 */
859 start_item = (m_selPosition / m_columns) * m_columns;
860 first_item = last_item = start_item;
861
862 /*
863 * Attempt to pick a start_button which will minimize the need
864 * for new button allocations.
865 */
866#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
867 start_button = std::max(m_buttonList.size() / 2, 0);
868#else
869 start_button = std::max(m_buttonList.size() / 2, static_cast<qsizetype>(0));
870#endif
871 start_button = (start_button / std::max(m_columns, 1)) * m_columns;
872
873 if (start_button < m_itemCount / 2 &&
874 m_itemCount - m_selPosition - 1 < m_buttonList.size() / 2)
875 start_button += m_columns;
876
877 first_button = last_button = start_button;
878
879 // Now do initial row layout again with our new knowledge
880 selected_column = selected_row = -1;
881
882 if (!DistributeRow(first_button, last_button,
883 first_item, last_item, selected_column,
884 skip_cols, grow_left, true, &col_widths,
885 height, 0, 0, col_cnt, wrapped))
886 {
887 delete[] col_widths;
888 return false;
889 }
890 }
891
892 if (selected_column != -1)
893 selected_row = 0;
894
895 row_heights.push_back(height);
896
898 top_height = bottom_height = (height / 2);
899 else
900 bottom_height = height;
901
903 break;
904
905 // As as many columns as will fit.
906 if (DistributeCols(first_button, last_button,
907 first_item, last_item,
908 selected_column, selected_row,
909 skip_cols, &col_widths, row_heights,
910 top_height, bottom_height, wrapped))
911 break; // Buttons fit on each row, so done
912
913 delete[] col_widths;
914 col_widths = nullptr;
915
916 --m_columns;
917 start_item = m_selPosition;
918 }
919
920 m_rows = row_heights.size();
921
922 LOG(VB_GUI, LOG_DEBUG,
923 QString("%1 rows, %2 columns fit inside parent area %3x%4")
924 .arg(m_rows).arg(m_columns).arg(m_contentsRect.width())
925 .arg(m_contentsRect.height()));
926
927 if (col_widths == nullptr)
928 return false;
929
930 int total = 0;
931 int left_spacing = 0;
932 int right_spacing = 0;
933 int top_spacing = 0;
934 int bottom_spacing = 0;
935 MythRect min_rect;
936 QString status_msg;
937
938 /*
939 * Calculate heights of buttons on each side of selected button
940 */
941 top_height = bottom_height = m_topRows = m_bottomRows = 0;
942
943 status_msg = "Row heights: ";
944
945 for (int row = 0; row < m_rows; ++row)
946 {
947 if (row != 0)
948 status_msg += ", ";
949
950 if (row == selected_row)
951 {
952 status_msg += '[';
953 top_height += (row_heights[row] / 2);
954 bottom_height += ((row_heights[row] / 2) + (row_heights[row] % 2));
955 }
956 else
957 {
958 if (bottom_height)
959 {
960 bottom_height += m_itemVertSpacing + row_heights[row];
961 ++m_bottomRows;
962 }
963 else
964 {
965 top_height += row_heights[row] + m_itemVertSpacing;
966 ++m_topRows;
967 }
968 }
969
970 status_msg += QString("%1").arg(row_heights[row]);
971
972 if (row == selected_row)
973 status_msg += ']';
974 }
975
976 /*
977 * How much extra space should there be between buttons?
978 */
980 {
981 // None
982 top_spacing = bottom_spacing = 0;
983 }
984 else
985 {
986 if (m_rows < 2)
987 {
988 // Equal space on both sides of single row
989 top_spacing = bottom_spacing =
990 (m_contentsRect.height() - top_height) / 2;
991 }
992 else
993 {
995 {
996 // Selected button needs to end up in the middle of area
997 top_spacing = m_topRows ? (m_contentsRect.height() / 2 -
998 top_height) / m_topRows : 0;
999 bottom_spacing = m_bottomRows ? (m_contentsRect.height() / 2 -
1000 bottom_height) / m_bottomRows : 0;
1001
1002 if (m_arrange == ArrangeSpread)
1003 {
1004 // Use same spacing on both sides of selected button
1005 if (!m_topRows || top_spacing > bottom_spacing)
1006 top_spacing = bottom_spacing;
1007 else
1008 bottom_spacing = top_spacing;
1009 }
1010 }
1011 else
1012 {
1013 // Buttons will be evenly spread out to fill entire area
1014 top_spacing = bottom_spacing = (m_contentsRect.height() -
1015 (top_height + bottom_height)) /
1017 }
1018 }
1019
1020 // Add in intra-button space size
1021 top_height += (top_spacing * m_topRows);
1022 bottom_height += (bottom_spacing * m_bottomRows);
1023 }
1024
1025 /*
1026 * Calculate top margin
1027 */
1028 int y = m_contentsRect.y();
1029
1030 if ((alignment & Qt::AlignVCenter) && m_arrange != ArrangeFill)
1031 {
1033 {
1034 // Adjust to compensate for top height less than bottom height
1035 y += std::max(bottom_height - top_height, 0);
1036 total = std::max(top_height, bottom_height) * 2;
1037 }
1038 else
1039 {
1040 total = top_height + bottom_height;
1041 }
1042
1043 // Adjust top margin so selected button ends up in the middle
1044 y += (std::max(m_contentsRect.height() - total, 2) / 2);
1045 }
1046 else if ((alignment & Qt::AlignBottom) && m_arrange == ArrangeStack)
1047 {
1048 // Adjust top margin so buttons are bottom justified
1049 y += std::max(m_contentsRect.height() -
1050 (top_height + bottom_height), 0);
1051 }
1052 min_rect.setY(y);
1053
1054 status_msg += QString(" spacing top %1 bottom %2 fixed %3 offset %4")
1055 .arg(top_spacing).arg(bottom_spacing)
1056 .arg(m_itemVertSpacing).arg(y);
1057
1058 LOG(VB_GUI, LOG_DEBUG, status_msg);
1059
1060 /*
1061 * Calculate width of buttons on each side of selected button
1062 */
1063 int left_width = 0;
1064 int right_width = 0;
1066
1067 status_msg = "Col widths: ";
1068
1069 for (int col = 0; col < m_columns; ++col)
1070 {
1071 if (col != 0)
1072 status_msg += ", ";
1073
1074 if (col == selected_column)
1075 {
1076 status_msg += '[';
1077 left_width += (col_widths[col] / 2);
1078 right_width += ((col_widths[col] / 2) + (col_widths[col] % 2));
1079 }
1080 else
1081 {
1082 if (right_width)
1083 {
1084 right_width += m_itemHorizSpacing + col_widths[col];
1086 }
1087 else
1088 {
1089 left_width += col_widths[col] + m_itemHorizSpacing;
1090 ++m_leftColumns;
1091 }
1092 }
1093
1094 status_msg += QString("%1").arg(col_widths[col]);
1095
1096 if (col == selected_column)
1097 status_msg += ']';
1098 }
1099
1100 /*
1101 * How much extra space should there be between buttons?
1102 */
1104 {
1105 // None
1106 left_spacing = right_spacing = 0;
1107 }
1108 else
1109 {
1110 if (m_columns < 2)
1111 {
1112 // Equal space on both sides of single column
1113 left_spacing = right_spacing =
1114 (m_contentsRect.width() - left_width) / 2;
1115 }
1116 else
1117 {
1119 {
1120 // Selected button needs to end up in the middle
1121 left_spacing = m_leftColumns ? (m_contentsRect.width() / 2 -
1122 left_width) / m_leftColumns : 0;
1123 right_spacing = m_rightColumns ? (m_contentsRect.width() / 2 -
1124 right_width) / m_rightColumns : 0;
1125
1126 if (m_arrange == ArrangeSpread)
1127 {
1128 // Use same spacing on both sides of selected button
1129 if (!m_leftColumns || left_spacing > right_spacing)
1130 left_spacing = right_spacing;
1131 else
1132 right_spacing = left_spacing;
1133 }
1134 }
1135 else
1136 {
1137 // Buttons will be evenly spread out to fill entire area
1138 left_spacing = right_spacing = (m_contentsRect.width() -
1139 (left_width + right_width)) /
1141 }
1142 }
1143
1144 // Add in intra-button space size
1145 left_width += (left_spacing * m_leftColumns);
1146 right_width += (right_spacing * m_rightColumns);
1147 }
1148
1149 /*
1150 * Calculate left margin
1151 */
1152 int x_init = m_contentsRect.x();
1153
1154 if ((alignment & Qt::AlignHCenter) && m_arrange != ArrangeFill)
1155 {
1157 {
1158 // Compensate for left being smaller than right
1159 x_init += std::max(right_width - left_width, 0);
1160 total = std::max(left_width, right_width) * 2;
1161 }
1162 else
1163 {
1164 total = left_width + right_width;
1165 }
1166
1167 // Adjust left margin so selected button ends up in the middle
1168 x_init += (std::max(m_contentsRect.width() - total, 2) / 2);
1169 }
1170 else if ((alignment & Qt::AlignRight) && m_arrange == ArrangeStack)
1171 {
1172 // Adjust left margin, so buttons are right justified
1173 x_init += std::max(m_contentsRect.width() -
1174 (left_width + right_width), 0);
1175 }
1176 min_rect.setX(x_init);
1177
1178 status_msg += QString(" spacing left %1 right %2 fixed %3 offset %4")
1179 .arg(left_spacing).arg(right_spacing)
1180 .arg(m_itemHorizSpacing).arg(x_init);
1181 LOG(VB_GUI, LOG_DEBUG, status_msg);
1182
1183 top_spacing += m_itemVertSpacing;
1184 bottom_spacing += m_itemVertSpacing;
1185 left_spacing += m_itemHorizSpacing;
1186 right_spacing += m_itemHorizSpacing;
1187
1188 // Calculate position of each button
1189 int buttonIdx = first_button - skip_cols;
1190 int x = 0;
1191 int x_adj = 0;
1192 int y_adj = 0;
1193
1194 int vertical_spacing = top_spacing;
1195
1196 for (int row = 0; row < m_rows; ++row)
1197 {
1198 x = x_init;
1199 int horizontal_spacing = left_spacing;
1200
1201 for (int col = 0; col < m_columns && buttonIdx <= last_button; ++col)
1202 {
1203 if (buttonIdx >= first_button)
1204 {
1205 MythUIStateType *realButton = m_buttonList[buttonIdx];
1206 auto *buttonstate = dynamic_cast<MythUIGroup *>
1207 (realButton->GetCurrentState());
1208 if (!buttonstate)
1209 break; // Not continue
1210
1211 MythRect area = buttonstate->GetArea();
1212
1213 // Center button within width of column
1214 if (alignment & Qt::AlignHCenter)
1215 x_adj = (col_widths[col] - minButtonWidth(area)) / 2;
1216 else if (alignment & Qt::AlignRight)
1217 x_adj = (col_widths[col] - minButtonWidth(area));
1218 else
1219 x_adj = 0;
1221 x_adj -= area.x(); // Negate button's own offset
1222
1223 // Center button within height of row.
1224 if (alignment & Qt::AlignVCenter)
1225 y_adj = (row_heights[row] - minButtonHeight(area)) / 2;
1226 else if (alignment & Qt::AlignBottom)
1227 y_adj = (row_heights[row] - minButtonHeight(area));
1228 else
1229 y_adj = 0;
1230 if (m_layout == LayoutVertical)
1231 y_adj -= area.y(); // Negate button's own offset
1232
1233 // Set position of button
1234 realButton->SetPosition(x + x_adj, y + y_adj);
1235 realButton->SetVisible(true);
1236
1237 if (col == selected_column)
1238 {
1239 horizontal_spacing = right_spacing;
1240 if (row == selected_row)
1241 realButton->MoveToTop();
1242 }
1243 }
1244 x += col_widths[col] + horizontal_spacing;
1245 ++buttonIdx;
1246 }
1247
1248 if (row == selected_row)
1249 vertical_spacing = bottom_spacing;
1250
1251 y += row_heights[row] + vertical_spacing;
1252 }
1253 min_rect.setWidth(x - min_rect.x());
1254 min_rect.setHeight(y - min_rect.y());
1255
1257
1258 // Hide buttons before first active button
1259 for (buttonIdx = 0; buttonIdx < first_button; ++buttonIdx)
1260 m_buttonList[buttonIdx]->SetVisible(false);
1261
1262 // Hide buttons after last active buttons.
1263 for (buttonIdx = m_maxVisible - 1; buttonIdx > last_button; --buttonIdx)
1264 m_buttonList[buttonIdx]->SetVisible(false);
1265
1266 // Set m_topPosition so arrows are displayed correctly.
1268 m_topPosition = static_cast<int>(m_itemsVisible < m_itemCount);
1269 else
1270 m_topPosition = first_item;
1271
1273 if (m_minSize.isValid())
1274 {
1275 // Record the minimal area needed for the button list
1276 SetMinArea(min_rect);
1277 }
1278
1279 delete[] col_widths;
1280 return true;
1281}
1282
1284{
1285 if (m_buttonList.empty())
1286 return;
1287
1288 int drawFromBottom = IsShadowing() && m_shadowDrawFromBottom ?
1290
1291 int button = 0;
1292
1293 switch (m_scrollStyle)
1294 {
1295 case ScrollCenter:
1296 case ScrollGroupCenter:
1297 m_topPosition = std::max(m_selPosition -
1298 (int)((float)m_itemsVisible / 2), 0);
1299 break;
1300 case ScrollFree:
1301 {
1302 int adjust = 0;
1303
1304 if (m_topPosition == -1 || m_keepSelAtBottom)
1305 {
1306 if (m_topPosition == -1)
1307 m_topPosition = 0;
1308
1310 adjust = 1 - m_itemsVisible;
1311 else
1312 adjust = m_columns - m_itemsVisible;
1313
1314 m_keepSelAtBottom = false;
1315 }
1316
1319 {
1321 m_topPosition = m_selPosition + adjust;
1322 else
1323 m_topPosition = (m_selPosition + adjust) /
1325 }
1326
1327 // Adjusted if last item is deleted
1328 if (((m_itemList.count() - m_topPosition) < m_itemsVisible) &&
1330 m_columns == 1)
1332
1333 m_topPosition = std::max(m_topPosition, 0);
1334 break;
1335 }
1336 }
1337
1338 QList<MythUIButtonListItem *>::iterator it = m_itemList.begin() +
1340
1342 {
1343 if (m_selPosition <= m_itemsVisible / 2)
1344 {
1345 button = (m_itemsVisible / 2) - m_selPosition;
1346
1347 if (m_wrapStyle == WrapItems && button > 0 &&
1349 {
1350 it = m_itemList.end() - button;
1351 button = 0;
1352 }
1353 }
1354 else if ((m_itemCount - m_selPosition) < (m_itemsVisible / 2))
1355 {
1356 it = m_itemList.begin() + m_selPosition - (m_itemsVisible / 2);
1357 }
1358 }
1359 else if (drawFromBottom && m_itemCount < m_itemsVisible)
1360 {
1361 button = m_itemsVisible - m_itemCount;
1362 }
1363
1364 for (int i = 0; i < button; ++i)
1365 m_buttonList[i]->SetVisible(false);
1366
1367 bool seenSelected = false;
1368
1369 MythUIStateType *realButton = nullptr;
1370 MythUIButtonListItem *buttonItem = nullptr;
1371
1372 if (it < m_itemList.begin())
1373 it = m_itemList.begin();
1374
1375 int curItem = it < m_itemList.end() ? GetItemPos(*it) : 0;
1376
1377 while (it < m_itemList.end() && button < m_itemsVisible)
1378 {
1379 realButton = m_buttonList[button];
1380 buttonItem = *it;
1381
1382 if (!realButton || !buttonItem)
1383 break;
1384
1385 bool selected = false;
1386
1387 if (!seenSelected && (curItem == m_selPosition))
1388 {
1389 seenSelected = true;
1390 selected = true;
1391 }
1392
1393 m_buttonToItem[button] = buttonItem;
1394 buttonItem->SetToRealButton(realButton, selected);
1395 realButton->SetVisible(true);
1396
1397 if (m_wrapStyle == WrapItems && it == (m_itemList.end() - 1) &&
1399 {
1400 it = m_itemList.begin();
1401 curItem = 0;
1402 }
1403 else
1404 {
1405 ++it;
1406 ++curItem;
1407 }
1408
1409 ++button;
1410 }
1411
1412 for (; button < m_itemsVisible; ++button)
1413 m_buttonList[button]->SetVisible(false);
1414}
1415
1417{
1418 if (m_selPosition < 0)
1419 m_selPosition = (m_wrapStyle > WrapNone) ? m_itemList.size() - 1 : 0;
1420 else if (m_selPosition >= m_itemList.size())
1421 m_selPosition = (m_wrapStyle > WrapNone) ? 0 : m_itemList.size() - 1;
1422}
1423
1425{
1426 if (!m_initialized)
1427 Init();
1428
1429 if (!m_initialized)
1430 return;
1431
1432 if (m_clearing)
1433 return;
1434
1435 m_needsUpdate = false;
1436
1437 // mark the visible buttons as invisible
1438 QMap<int, MythUIButtonListItem*>::const_iterator i = m_buttonToItem.constBegin();
1439 while (i != m_buttonToItem.constEnd())
1440 {
1441 if (i.value())
1442 i.value()->setVisible(false);
1443 ++i;
1444 }
1445
1446 // set topitem, top position
1448 m_buttonToItem.clear();
1449
1450 if (m_arrange == ArrangeFixed)
1452 else
1454
1455 updateLCD();
1456
1457 m_needsUpdate = false;
1458
1459 if (!m_downArrow || !m_upArrow)
1460 return;
1461
1462 if (m_itemCount == 0)
1463 {
1466 }
1467 else
1468 {
1469 if (m_topPosition != 0)
1471 else
1473
1476 else
1478
1481 }
1482}
1483
1485{
1486 if (item)
1487 emit itemVisible(item);
1488}
1489
1491{
1492 bool wasEmpty = m_itemList.isEmpty();
1493
1494 if (listPosition >= 0 && listPosition <= m_itemList.count())
1495 {
1496 m_itemList.insert(listPosition, item);
1497
1498 if (listPosition <= m_selPosition)
1499 ++m_selPosition;
1500
1501 if (listPosition <= m_topPosition)
1502 ++m_topPosition;
1503 }
1504 else
1505 {
1506 m_itemList.append(item);
1507 }
1508
1509 ++m_itemCount;
1510
1511 if (wasEmpty)
1512 {
1514 emit itemSelected(item);
1515 emit DependChanged(false);
1516 }
1517
1518 Update();
1519}
1520
1522{
1523 if (m_clearing)
1524 return;
1525
1526 int curIndex = m_itemList.indexOf(item);
1527
1528 if (curIndex == -1)
1529 return;
1530
1531 QMap<int, MythUIButtonListItem*>::iterator it = m_buttonToItem.begin();
1532 while (it != m_buttonToItem.end())
1533 {
1534 if (it.value() == item)
1535 {
1536 m_buttonToItem.erase(it);
1537 break;
1538 }
1539 ++it;
1540 }
1541
1542 if (curIndex < m_topPosition &&
1543 m_topPosition > 0)
1544 {
1545 // The removed item is before the visible part, move
1546 // everything up 1. The visible part shouldn't appear to
1547 // change.
1548 --m_topPosition;
1549 --m_selPosition;
1550 }
1551 else if (curIndex < m_selPosition ||
1552 (m_selPosition == m_itemCount - 1 &&
1553 m_selPosition > 0))
1554 {
1555 // The removed item is visible and before the selected item or
1556 // the selected item is the last item but not the only item,
1557 // move the selected item up 1.
1558 --m_selPosition;
1559 }
1560
1561 m_itemList.removeAt(curIndex);
1562 --m_itemCount;
1563
1564 Update();
1565
1568 else
1569 emit itemSelected(nullptr);
1570
1571 if (IsEmpty())
1572 emit DependChanged(true);
1573}
1574
1575void MythUIButtonList::SetValueByData(const QVariant& data)
1576{
1577 if (!m_initialized)
1578 Init();
1579
1580 for (auto *item : std::as_const(m_itemList))
1581 {
1582 if (item->GetData() == data)
1583 {
1584 SetItemCurrent(item);
1585 return;
1586 }
1587 }
1588}
1589
1591{
1592 int newIndex = m_itemList.indexOf(item);
1593 SetItemCurrent(newIndex);
1594}
1595
1597{
1598 if (!m_initialized)
1599 Init();
1600
1601 if (current == -1 || current >= m_itemList.size())
1602 return;
1603
1604 if (!m_itemList.at(current)->isEnabled())
1605 return;
1606
1607 if (current == m_selPosition &&
1608 (topPosition == -1 || topPosition == m_topPosition))
1609 return;
1610
1611 m_topPosition = topPosition;
1612
1613 if (topPosition > 0 && m_layout == LayoutGrid)
1614 m_topPosition -= (topPosition % m_columns);
1615
1617
1618 Update();
1619
1621}
1622
1624{
1625 if (m_itemList.isEmpty() || m_selPosition >= m_itemList.size() ||
1626 m_selPosition < 0)
1627 return nullptr;
1628
1629 return m_itemList.at(m_selPosition);
1630}
1631
1633{
1635
1636 if (item)
1637 return item->GetText().toInt();
1638
1639 return 0;
1640}
1641
1643{
1645
1646 if (item)
1647 return item->GetText();
1648
1649 return {};
1650}
1651
1653{
1655
1656 if (item)
1657 return item->GetData();
1658
1659 return {};
1660}
1661
1663{
1664 if (m_contentsRect.isValid())
1665 return m_contentsRect;
1666 return m_area;
1667}
1668
1670{
1671 if (!m_itemList.empty())
1672 return m_itemList[0];
1673
1674 return nullptr;
1675}
1676
1678const
1679{
1680 QListIterator<MythUIButtonListItem *> it(m_itemList);
1681
1682 if (!it.findNext(item))
1683 return nullptr;
1684
1685 return it.previous();
1686}
1687
1689{
1690 return m_itemCount;
1691}
1692
1694{
1695 if (m_needsUpdate)
1696 {
1699 }
1700
1701 return m_itemsVisible;
1702}
1703
1705{
1706 return m_itemCount <= 0;
1707}
1708
1710{
1711 if (pos < 0 || pos >= m_itemList.size())
1712 return nullptr;
1713
1714 return m_itemList.at(pos);
1715}
1716
1718{
1719 if (!m_initialized)
1720 Init();
1721
1722 for (auto *item : std::as_const(m_itemList))
1723 {
1724 if (item->GetData() == data)
1725 return item;
1726 }
1727
1728 return nullptr;
1729}
1730
1732{
1733 if (!item)
1734 return -1;
1735
1736 return m_itemList.indexOf(item);
1737}
1738
1739void MythUIButtonList::InitButton(int itemIdx, MythUIStateType* & realButton,
1740 MythUIButtonListItem* & buttonItem)
1741{
1742 buttonItem = m_itemList[itemIdx];
1743
1744 if (m_maxVisible == 0)
1745 {
1746 QString name("buttonlist button 0");
1747 auto *button = new MythUIStateType(this, name);
1748 button->CopyFrom(m_buttontemplate);
1749 button->ConnectDependants(true);
1750 m_buttonList.append(button);
1751 ++m_maxVisible;
1752 }
1753
1754 realButton = m_buttonList[0];
1755 m_buttonToItem[0] = buttonItem;
1756}
1757
1758/*
1759 * PageUp and PageDown are helpers when Dynamic layout is being used.
1760 *
1761 * When buttons are layed out dynamically, the number of buttons on the next
1762 * page, may not equal the number of buttons on the current page. Dynamic
1763 * layout is always center-weighted, so attempt to figure out which button
1764 * is near the middle on the next page.
1765 */
1766
1768{
1769 int pos = m_selPosition;
1770 int total = 0;
1771
1772 /*
1773 * /On the new page/
1774 * If the number of buttons before the selected button does not equal
1775 * the number of buttons after the selected button, this logic can
1776 * undershoot the new selected button. That is better than overshooting
1777 * though.
1778 *
1779 * To fix this would require laying out the new page and then figuring
1780 * out which button should be selected, but this is already complex enough.
1781 */
1782
1784 {
1785 pos -= (m_leftColumns + 1);
1786
1787 int max_width = m_contentsRect.width() / 2;
1788
1789 for (; pos >= 0; --pos)
1790 {
1791 MythUIStateType *realButton = nullptr;
1792 MythUIButtonListItem *buttonItem = nullptr;
1793 InitButton(pos, realButton, buttonItem);
1794 buttonItem->SetToRealButton(realButton, true);
1795 auto *buttonstate = dynamic_cast<MythUIGroup *>
1796 (realButton->GetCurrentState());
1797
1798 if (buttonstate == nullptr)
1799 {
1800 LOG(VB_GENERAL, LOG_ERR,
1801 "PageUp: Failed to query buttonlist state");
1802 return pos;
1803 }
1804
1805 if (total + m_itemHorizSpacing +
1806 (buttonstate->GetArea().width() / 2) >= max_width)
1807 return pos + 1;
1808
1809 buttonItem->SetToRealButton(realButton, false);
1810 buttonstate = dynamic_cast<MythUIGroup *>
1811 (realButton->GetCurrentState());
1812 if (buttonstate)
1813 total += m_itemHorizSpacing + buttonstate->GetArea().width();
1814 }
1815
1816 return 0;
1817 }
1818
1819 // Grid or Vertical
1820 int dec = 1;
1821
1822 if (m_layout == LayoutGrid)
1823 {
1824 /*
1825 * Adjusting using bottomRow:TopRow only works if new page
1826 * has the same ratio as the previous page, but that is common
1827 * with the grid layout, so go for it. If themers start doing
1828 * grids where this is not true, then this will need to be modified.
1829 */
1830 pos -= (m_columns * (m_topRows + 2 +
1831 std::max(m_bottomRows - m_topRows, 0)));
1832 dec = m_columns;
1833 }
1834 else
1835 {
1836 pos -= (m_topRows + 1);
1837 dec = 1;
1838 }
1839
1840 int max_height = m_contentsRect.height() / 2;
1841
1842 for (; pos >= 0; pos -= dec)
1843 {
1844 MythUIStateType *realButton = nullptr;
1845 MythUIButtonListItem *buttonItem = nullptr;
1846 InitButton(pos, realButton, buttonItem);
1847 buttonItem->SetToRealButton(realButton, true);
1848 auto *buttonstate = dynamic_cast<MythUIGroup *>
1849 (realButton->GetCurrentState());
1850
1851 if (buttonstate == nullptr)
1852 {
1853 LOG(VB_GENERAL, LOG_ERR,
1854 "PageUp: Failed to query buttonlist state");
1855 return pos;
1856 }
1857
1858 if (total + m_itemHorizSpacing +
1859 (buttonstate->GetArea().height() / 2) >= max_height)
1860 return pos + dec;
1861
1862 buttonItem->SetToRealButton(realButton, false);
1863 buttonstate = dynamic_cast<MythUIGroup *>
1864 (realButton->GetCurrentState());
1865 if (buttonstate)
1866 total += m_itemHorizSpacing + buttonstate->GetArea().height();
1867 }
1868
1869 return 0;
1870}
1871
1873{
1874 int pos = m_selPosition;
1875 int num_items = m_itemList.size();
1876 int total = 0;
1877
1878 /*
1879 * /On the new page/
1880 * If the number of buttons before the selected button does not equal
1881 * the number of buttons after the selected button, this logic can
1882 * undershoot the new selected button. That is better than overshooting
1883 * though.
1884 *
1885 * To fix this would require laying out the new page and then figuring
1886 * out which button should be selected, but this is already complex enough.
1887 */
1888
1890 {
1891 pos += (m_rightColumns + 1);
1892
1893 int max_width = m_contentsRect.width() / 2;
1894
1895 for (; pos < num_items; ++pos)
1896 {
1897 MythUIStateType *realButton = nullptr;
1898 MythUIButtonListItem *buttonItem = nullptr;
1899 InitButton(pos, realButton, buttonItem);
1900 buttonItem->SetToRealButton(realButton, true);
1901 auto *buttonstate = dynamic_cast<MythUIGroup *>
1902 (realButton->GetCurrentState());
1903
1904 if (buttonstate == nullptr)
1905 {
1906 LOG(VB_GENERAL, LOG_ERR,
1907 "PageDown: Failed to query buttonlist state");
1908 return pos;
1909 }
1910
1911 if (total + m_itemHorizSpacing +
1912 (buttonstate->GetArea().width() / 2) >= max_width)
1913 return pos - 1;
1914
1915 buttonItem->SetToRealButton(realButton, false);
1916 buttonstate = dynamic_cast<MythUIGroup *>
1917 (realButton->GetCurrentState());
1918 if (buttonstate)
1919 total += m_itemHorizSpacing + buttonstate->GetArea().width();
1920 }
1921
1922 return num_items - 1;
1923 }
1924
1925 // Grid or Vertical
1926 int inc = 1;
1927
1928 if (m_layout == LayoutGrid)
1929 {
1930 /*
1931 * Adjusting using bottomRow:TopRow only works if new page
1932 * has the same ratio as the previous page, but that is common
1933 * with the grid layout, so go for it. If themers start doing
1934 * grids where this is not true, then this will need to be modified.
1935 */
1936 pos += (m_columns * (m_bottomRows + 2 +
1937 std::max(m_topRows - m_bottomRows, 0)));
1938 inc = m_columns;
1939 }
1940 else
1941 {
1942 pos += (m_bottomRows + 1);
1943 inc = 1;
1944 }
1945
1946 int max_height = m_contentsRect.height() / 2;
1947
1948 for (; pos < num_items; pos += inc)
1949 {
1950 MythUIStateType *realButton = nullptr;
1951 MythUIButtonListItem *buttonItem = nullptr;
1952 InitButton(pos, realButton, buttonItem);
1953 buttonItem->SetToRealButton(realButton, true);
1954 auto *buttonstate = dynamic_cast<MythUIGroup *>
1955 (realButton->GetCurrentState());
1956
1957 if (!buttonstate)
1958 {
1959 LOG(VB_GENERAL, LOG_ERR,
1960 "PageDown: Failed to query buttonlist state");
1961 return pos;
1962 }
1963
1964 if (total + m_itemHorizSpacing +
1965 (buttonstate->GetArea().height() / 2) >= max_height)
1966 return pos - inc;
1967
1968 buttonItem->SetToRealButton(realButton, false);
1969 buttonstate = dynamic_cast<MythUIGroup *>
1970 (realButton->GetCurrentState());
1971 if (buttonstate)
1972 total += m_itemHorizSpacing + buttonstate->GetArea().height();
1973 }
1974
1975 return num_items - 1;
1976}
1977
1979{
1980 int pos = m_selPosition;
1981
1982 if (pos == -1 || m_itemList.isEmpty() || !m_initialized)
1983 return false;
1984
1985 switch (unit)
1986 {
1987 case MoveItem:
1988 if (m_selPosition > 0)
1989 --m_selPosition;
1990 else if (m_wrapStyle > WrapNone)
1991 m_selPosition = m_itemList.size() - 1;
1992 else if (m_wrapStyle == WrapCaptive)
1993 return true;
1994
1995 FindEnabledUp(unit);
1996
1997 break;
1998
1999 case MoveColumn:
2000 if (pos % m_columns > 0)
2001 {
2002 --m_selPosition;
2003 }
2004 else if (m_wrapStyle == WrapFlowing)
2005 {
2006 if (m_selPosition == 0)
2007 --m_selPosition = m_itemList.size() - 1;
2008 else
2009 --m_selPosition;
2010 }
2011 else if (m_wrapStyle > WrapNone)
2012 {
2013 m_selPosition = pos + (m_columns - 1);
2014 }
2015 else if (m_wrapStyle == WrapCaptive)
2016 {
2017 return true;
2018 }
2019
2020 FindEnabledUp(unit);
2021
2022 break;
2023
2024 case MoveRow:
2026 {
2028 if (m_selPosition < 0)
2029 m_selPosition += m_itemList.size();
2030 else
2031 m_selPosition %= m_itemList.size();
2032 }
2033 else if ((pos - m_columns) >= 0)
2034 {
2036 }
2037 else if (m_wrapStyle > WrapNone)
2038 {
2039 m_selPosition = (((m_itemList.size() - 1) / m_columns) *
2040 m_columns) + pos;
2041
2042 if ((m_selPosition / m_columns)
2043 < ((m_itemList.size() - 1) / m_columns))
2044 m_selPosition = m_itemList.size() - 1;
2045
2046 if (m_layout == LayoutVertical)
2047 m_topPosition = std::max(0, m_selPosition - m_itemsVisible + 1);
2048 }
2049 else if (m_wrapStyle == WrapCaptive)
2050 {
2051 return true;
2052 }
2053
2054 FindEnabledUp(unit);
2055
2056 break;
2057
2058 case MovePage:
2059 if (m_arrange == ArrangeFixed)
2061 else
2063
2064 FindEnabledUp(unit);
2065
2066 break;
2067
2068 case MoveMid:
2069 m_selPosition = m_itemList.size() / 2;
2070 FindEnabledUp(unit);
2071 break;
2072
2073 case MoveMax:
2074 m_selPosition = 0;
2075 FindEnabledUp(unit);
2076 break;
2077
2078 case MoveByAmount:
2079 for (uint i = 0; i < amount; ++i)
2080 {
2081 if (m_selPosition > 0)
2082 --m_selPosition;
2083 else if (m_wrapStyle > WrapNone)
2084 m_selPosition = m_itemList.size() - 1;
2085 }
2086
2087 FindEnabledUp(unit);
2088
2089 break;
2090 }
2091
2093
2094 if (pos != m_selPosition)
2095 {
2096 Update();
2098 }
2099 else
2100 {
2101 return false;
2102 }
2103
2104 return true;
2105}
2106
2107
2112{
2113 if (m_selPosition < 0 || m_selPosition >= m_itemList.size() ||
2114 m_itemList.at(m_selPosition)->isEnabled())
2115 return;
2116
2117 int step = (unit == MoveRow) ? m_columns : 1;
2118 if (unit == MoveRow && m_wrapStyle == WrapFlowing)
2119 unit = MoveItem;
2120 if (unit == MoveColumn)
2121 {
2122 while (m_selPosition < m_itemList.size() &&
2123 (m_selPosition + 1) % m_columns > 0 &&
2124 !m_itemList.at(m_selPosition)->isEnabled())
2125 ++m_selPosition;
2126
2127 if (m_itemList.at(m_selPosition)->isEnabled())
2128 return;
2129
2130 if (m_wrapStyle > WrapNone)
2131 {
2133 while ((m_selPosition + 1) % m_columns > 0 &&
2134 !m_itemList.at(m_selPosition)->isEnabled())
2135 ++m_selPosition;
2136 }
2137 }
2138 else
2139 {
2140 while (!m_itemList.at(m_selPosition)->isEnabled() &&
2141 (m_selPosition < m_itemList.size() - step))
2142 m_selPosition += step;
2143
2144 if (!m_itemList.at(m_selPosition)->isEnabled() &&
2146 {
2147 m_selPosition = (m_selPosition + step) % m_itemList.size();
2148
2149 while (!m_itemList.at(m_selPosition)->isEnabled() &&
2150 (m_selPosition < m_itemList.size() - step))
2151 m_selPosition += step;
2152 }
2153 }
2154}
2155
2157{
2158 if (m_selPosition < 0 || m_selPosition >= m_itemList.size() ||
2159 m_itemList.at(m_selPosition)->isEnabled())
2160 return;
2161
2162 int step = (unit == MoveRow) ? m_columns : 1;
2163 if (unit == MoveRow && m_wrapStyle == WrapFlowing)
2164 unit = MoveItem;
2165 if (unit == MoveColumn)
2166 {
2167 while (m_selPosition > 0 && (m_selPosition - 1) % m_columns > 0 &&
2168 !m_itemList.at(m_selPosition)->isEnabled())
2169 --m_selPosition;
2170
2171 if (m_itemList.at(m_selPosition)->isEnabled())
2172 return;
2173
2174 if (m_wrapStyle > WrapNone)
2175 {
2177 while ((m_selPosition - 1) % m_columns > 0 &&
2178 !m_itemList.at(m_selPosition)->isEnabled())
2179 --m_selPosition;
2180 }
2181 }
2182 else
2183 {
2184 while (!m_itemList.at(m_selPosition)->isEnabled() &&
2185 (m_selPosition - step >= 0))
2186 m_selPosition -= step;
2187
2188 if (!m_itemList.at(m_selPosition)->isEnabled() &&
2190 {
2191 m_selPosition = m_itemList.size() - 1;
2192
2193 while (m_selPosition > 0 &&
2194 !m_itemList.at(m_selPosition)->isEnabled() &&
2195 (m_selPosition - step >= 0))
2196 m_selPosition -= step;
2197 }
2198 }
2199}
2200
2201
2203{
2204 int pos = m_selPosition;
2205
2206 if (pos == -1 || m_itemList.isEmpty() || !m_initialized)
2207 return false;
2208
2209 switch (unit)
2210 {
2211 case MoveItem:
2212 if (m_selPosition < m_itemList.size() - 1)
2213 ++m_selPosition;
2214 else if (m_wrapStyle > WrapNone)
2215 m_selPosition = 0;
2216 else if (m_wrapStyle == WrapCaptive)
2217 return true;
2218
2219 FindEnabledDown(unit);
2220
2221 break;
2222
2223 case MoveColumn:
2224 if ((pos + 1) % m_columns > 0)
2225 {
2226 ++m_selPosition;
2227 }
2228 else if (m_wrapStyle == WrapFlowing)
2229 {
2230 if (m_selPosition < m_itemList.size() - 1)
2231 ++m_selPosition;
2232 else
2233 m_selPosition = 0;
2234 }
2235 else if (m_wrapStyle > WrapNone)
2236 {
2237 m_selPosition = pos - (m_columns - 1);
2238 }
2239 else if (m_wrapStyle == WrapCaptive)
2240 {
2241 return true;
2242 }
2243
2244 FindEnabledDown(unit);
2245
2246 break;
2247
2248 case MoveRow:
2249 if (m_itemList.empty() || m_columns < 1)
2250 return true;
2252 {
2254 m_selPosition %= m_itemList.size();
2255 }
2256 else if (((m_itemList.size() - 1) / std::max(m_columns, 0))
2257 > (pos / m_columns))
2258 {
2260 if (m_selPosition >= m_itemList.size())
2261 m_selPosition = m_itemList.size() - 1;
2262 }
2263 else if (m_wrapStyle > WrapNone)
2264 {
2265 m_selPosition = (pos % m_columns);
2266 }
2267 else if (m_wrapStyle == WrapCaptive)
2268 {
2269 return true;
2270 }
2271
2272 FindEnabledDown(unit);
2273
2274 break;
2275
2276 case MovePage:
2277 if (m_arrange == ArrangeFixed)
2278 {
2279 m_selPosition = std::min(m_itemCount - 1,
2281 }
2282 else
2283 {
2285 }
2286
2287 FindEnabledDown(unit);
2288
2289 break;
2290
2291 case MoveMax:
2293 FindEnabledDown(unit);
2294 break;
2295
2296 case MoveByAmount:
2297 for (uint i = 0; i < amount; ++i)
2298 {
2299 if (m_selPosition < m_itemList.size() - 1)
2300 ++m_selPosition;
2301 else if (m_wrapStyle > WrapNone)
2302 m_selPosition = 0;
2303 }
2304 FindEnabledDown(unit);
2305 break;
2306
2307 case MoveMid:
2308 break;
2309 }
2310
2312
2313 if (pos != m_selPosition)
2314 {
2315 m_keepSelAtBottom = true;
2316 Update();
2318 }
2319 else
2320 {
2321 return false;
2322 }
2323
2324 return true;
2325}
2326
2327bool MythUIButtonList::MoveToNamedPosition(const QString &position_name)
2328{
2329 if (!m_initialized)
2330 Init();
2331
2332 if (m_selPosition < 0 || m_itemList.isEmpty() || !m_initialized)
2333 return false;
2334
2335 bool found_it = false;
2336 int selectedPosition = 0;
2337 QList<MythUIButtonListItem *>::iterator it = m_itemList.begin();
2338
2339 while (it != m_itemList.end())
2340 {
2341 if ((*it)->GetText() == position_name)
2342 {
2343 found_it = true;
2344 break;
2345 }
2346
2347 ++it;
2348 ++selectedPosition;
2349 }
2350
2351 if (!found_it || m_selPosition == selectedPosition)
2352 return false;
2353
2354 SetItemCurrent(selectedPosition);
2355 return true;
2356}
2357
2359{
2360 if (GetItemCurrent() != item)
2361 return false;
2362
2363 if (item == m_itemList.first() && up)
2364 return false;
2365
2366 if (item == m_itemList.last() && !up)
2367 return false;
2368
2369 int oldpos = m_selPosition;
2370 int insertat = 0;
2371 bool dolast = false;
2372
2373 if (up)
2374 {
2375 insertat = m_selPosition - 1;
2376
2377 if (item == m_itemList.last())
2378 dolast = true;
2379 else
2380 ++m_selPosition;
2381
2382 if (item == m_itemList.at(m_topPosition))
2383 ++m_topPosition;
2384 }
2385 else
2386 {
2387 insertat = m_selPosition + 1;
2388 }
2389
2390 m_itemList.removeAt(oldpos);
2391 m_itemList.insert(insertat, item);
2392
2393 if (up)
2394 {
2395 MoveUp();
2396
2397 if (!dolast)
2398 MoveUp();
2399 }
2400 else
2401 {
2402 MoveDown();
2403 }
2404
2405 return true;
2406}
2407
2409{
2410 QMutableListIterator<MythUIButtonListItem *> it(m_itemList);
2411
2412 while (it.hasNext())
2413 it.next()->setChecked(state);
2414}
2415
2417{
2418 if (m_initialized)
2419 return;
2420
2421 m_upArrow = dynamic_cast<MythUIStateType *>(GetChild("upscrollarrow"));
2422 m_downArrow = dynamic_cast<MythUIStateType *>(GetChild("downscrollarrow"));
2423 m_scrollBar = dynamic_cast<MythUIScrollBar *>(GetChild("scrollbar"));
2424
2425 if (m_upArrow)
2426 m_upArrow->SetVisible(true);
2427
2428 if (m_downArrow)
2429 m_downArrow->SetVisible(true);
2430
2431 if (m_scrollBar)
2433
2435
2436 m_buttontemplate = dynamic_cast<MythUIStateType *>(GetChild("buttonitem"));
2437
2438 if (!m_buttontemplate)
2439 {
2440 LOG(VB_GENERAL, LOG_ERR, QString("(%1) Statetype buttonitem is "
2441 "required in mythuibuttonlist: %2")
2442 .arg(GetXMLLocation(), objectName()));
2443 return;
2444 }
2445
2447
2448 MythRect buttonItemArea;
2449
2450 MythUIGroup *buttonActiveState = dynamic_cast<MythUIGroup *>
2451 (m_buttontemplate->GetState("active"));
2452
2453 if (buttonActiveState)
2454 buttonItemArea = buttonActiveState->GetArea();
2455 else
2456 buttonItemArea = m_buttontemplate->GetArea();
2457
2458 buttonItemArea.CalculateArea(m_contentsRect);
2459
2460 m_itemHeight = buttonItemArea.height();
2461 m_itemWidth = buttonItemArea.width();
2462
2463 /*
2464 * If fixed spacing is defined, then use the "active" state size
2465 * to predictively determine the position of each button.
2466 */
2467 if (m_arrange == ArrangeFixed)
2468 {
2469
2471
2472 int col = 1;
2473 int row = 1;
2474
2475 for (int i = 0; i < m_itemsVisible; ++i)
2476 {
2477 QString name = QString("buttonlist button %1").arg(i);
2478 auto *button = new MythUIStateType(this, name);
2479 button->CopyFrom(m_buttontemplate);
2480 button->ConnectDependants(true);
2481
2482 if (col > m_columns)
2483 {
2484 col = 1;
2485 ++row;
2486 }
2487
2488 button->SetPosition(GetButtonPosition(col, row));
2489 ++col;
2490
2491 m_buttonList.push_back(button);
2492 }
2493 }
2494
2495 // The following is pretty much a hack for the benefit of MythGallery
2496 // it scales images based on the button size and we need to give it the
2497 // largest button state so that the images are not too small
2498 // This can be removed once the disk based image caching is added to
2499 // mythui, since the mythgallery thumbnail generator can be ditched.
2500 MythUIGroup *buttonSelectedState = dynamic_cast<MythUIGroup *>
2501 (m_buttontemplate->GetState("selected"));
2502
2503 if (buttonSelectedState)
2504 {
2505 MythRect itemArea = buttonSelectedState->GetArea();
2506 itemArea.CalculateArea(m_contentsRect);
2507
2508 m_itemHeight = std::max(m_itemHeight, itemArea.height());
2509
2510 m_itemWidth = std::max(m_itemWidth, itemArea.width());
2511 }
2512
2513 // End Hack
2514
2515 m_initialized = true;
2516}
2517
2519{
2520 if (!m_initialized)
2521 Init();
2522
2523 return static_cast<uint>(m_itemWidth);
2524}
2525
2527{
2528 if (!m_initialized)
2529 Init();
2530
2531 return static_cast<uint>(m_itemHeight);
2532}
2533
2538{
2539 QStringList actions;
2540 bool handled = false;
2541 handled = GetMythMainWindow()->TranslateKeyPress("Global", event, actions);
2542
2543 // Handle action remappings
2544 for (const QString& action : std::as_const(actions))
2545 {
2546 if (!m_actionRemap.contains(action))
2547 continue;
2548
2549 QString key = m_actionRemap[action];
2550 if (key.isEmpty())
2551 return true;
2552
2553 QKeySequence a(key);
2554 if (a.isEmpty())
2555 continue;
2556
2557#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
2558 int keyCode = a[0];
2559 Qt::KeyboardModifiers modifiers = Qt::NoModifier;
2560 QStringList parts = key.split('+');
2561 for (int j = 0; j < parts.count(); ++j)
2562 {
2563 if (parts[j].toUpper() == "CTRL")
2564 modifiers |= Qt::ControlModifier;
2565 if (parts[j].toUpper() == "SHIFT")
2566 modifiers |= Qt::ShiftModifier;
2567 if (parts[j].toUpper() == "ALT")
2568 modifiers |= Qt::AltModifier;
2569 if (parts[j].toUpper() == "META")
2570 modifiers |= Qt::MetaModifier;
2571 }
2572#else
2573 int keyCode = a[0].key();
2574 Qt::KeyboardModifiers modifiers = a[0].keyboardModifiers();
2575#endif
2576
2577 QCoreApplication::postEvent(
2579 new QKeyEvent(QEvent::KeyPress, keyCode, modifiers, key));
2580 QCoreApplication::postEvent(
2582 new QKeyEvent(QEvent::KeyRelease, keyCode, modifiers, key));
2583
2584 return true;
2585 }
2586
2587 // handle actions for this container
2588 for (int i = 0; i < actions.size() && !handled; ++i)
2589 {
2590 const QString& action = actions[i];
2591 handled = true;
2592
2593 if (action == "UP")
2594 {
2595 if ((m_layout == LayoutVertical) || (m_layout == LayoutGrid))
2596 handled = MoveUp(MoveRow);
2597 else
2598 handled = false;
2599 }
2600 else if (action == "DOWN")
2601 {
2602 if ((m_layout == LayoutVertical) || (m_layout == LayoutGrid))
2603 handled = MoveDown(MoveRow);
2604 else
2605 handled = false;
2606 }
2607 else if (action == "RIGHT")
2608 {
2610 handled = MoveDown(MoveItem);
2611 else if (m_layout == LayoutGrid)
2612 {
2614 handled = MoveDown(MoveColumn);
2615 else
2616 handled = MoveDown(MoveItem);
2617 }
2618 else
2619 {
2620 handled = false;
2621 }
2622 }
2623 else if (action == "LEFT")
2624 {
2626 handled = MoveUp(MoveItem);
2627 else if (m_layout == LayoutGrid)
2628 {
2630 handled = MoveUp(MoveColumn);
2631 else
2632 handled = MoveUp(MoveItem);
2633 }
2634 else
2635 {
2636 handled = false;
2637 }
2638 }
2639 else if (action == "PAGEUP")
2640 {
2642 }
2643 else if (action == "PAGEDOWN")
2644 {
2646 }
2647 else if (action == "PAGETOP")
2648 {
2649 MoveUp(MoveMax);
2650 }
2651 else if (action == "PAGEMIDDLE")
2652 {
2653 MoveUp(MoveMid);
2654 }
2655 else if (action == "PAGEBOTTOM")
2656 {
2658 }
2659 else if (action == "SELECT")
2660 {
2662
2663 if (item && item->isEnabled())
2664 emit itemClicked(item);
2665 }
2666 else if (action == "SEARCH")
2667 {
2669 }
2670 else
2671 {
2672 handled = false;
2673 }
2674 }
2675
2676 return handled;
2677}
2678
2683{
2684 bool handled = false;
2685
2686 switch (event->GetGesture())
2687 {
2689 {
2690 // We want the relative position of the click
2691 QPoint position = event->GetPosition() -
2693
2694 MythUIType *type = GetChildAt(position, false, false);
2695
2696 if (!type)
2697 return false;
2698
2699 auto *object = dynamic_cast<MythUIStateType *>(type);
2700 if (object)
2701 {
2702 handled = true;
2703 QString name = object->objectName();
2704
2705 if (name == "upscrollarrow")
2706 {
2708 }
2709 else if (name == "downscrollarrow")
2710 {
2712 }
2713 else if (name.startsWith("buttonlist button"))
2714 {
2715 int pos = name.section(' ', 2, 2).toInt();
2716 MythUIButtonListItem *item = m_buttonToItem.value(pos);
2717
2718 if (item)
2719 {
2720 if (item == GetItemCurrent())
2721 emit itemClicked(item);
2722 else
2723 SetItemCurrent(item);
2724 }
2725 }
2726 else
2727 {
2728 handled = false;
2729 }
2730 }
2731 }
2732 break;
2733
2737 if ((m_layout == LayoutVertical) || (m_layout == LayoutGrid))
2738 handled = MoveUp(MoveRow);
2739 break;
2740
2744 if ((m_layout == LayoutVertical) || (m_layout == LayoutGrid))
2745 handled = MoveDown(MoveRow);
2746 break;
2747
2750 handled = MoveDown(MoveItem);
2751 else if (m_layout == LayoutGrid)
2752 {
2754 handled = MoveDown(MoveColumn);
2755 else
2756 handled = MoveDown(MoveItem);
2757 }
2758 break;
2759
2762 handled = MoveUp(MoveItem);
2763 else if (m_layout == LayoutGrid)
2764 {
2766 handled = MoveUp(MoveColumn);
2767 else
2768 handled = MoveUp(MoveItem);
2769 }
2770 break;
2771
2772 default:
2773 break;
2774 }
2775
2776 return handled;
2777}
2778
2779class NextButtonListPageEvent : public QEvent
2780{
2781 public:
2782 NextButtonListPageEvent(int start, int pageSize) :
2783 QEvent(kEventType), m_start(start), m_pageSize(pageSize) {}
2784 const int m_start;
2785 const int m_pageSize;
2786 static const Type kEventType;
2787};
2788
2789const QEvent::Type NextButtonListPageEvent::kEventType =
2790 (QEvent::Type) QEvent::registerEventType();
2791
2793{
2794 if (event->type() == NextButtonListPageEvent::kEventType)
2795 {
2796 if (auto *npe = dynamic_cast<NextButtonListPageEvent*>(event); npe)
2797 {
2798 int cur = npe->m_start;
2799 for (; cur < npe->m_start + npe->m_pageSize && cur < GetCount(); ++cur)
2800 {
2801 const int loginterval = (cur < 1000 ? 100 : 500);
2802 if (cur > 200 && cur % loginterval == 0)
2803 LOG(VB_GUI, LOG_INFO,
2804 QString("Build background buttonlist item %1").arg(cur));
2805 emit itemLoaded(GetItemAt(cur));
2806 }
2807 m_nextItemLoaded = cur;
2808 if (cur < GetCount())
2809 LoadInBackground(cur, npe->m_pageSize);
2810 }
2811 }
2812}
2813
2814void MythUIButtonList::LoadInBackground(int start, int pageSize)
2815{
2816 m_nextItemLoaded = start;
2817 QCoreApplication::
2818 postEvent(this, new NextButtonListPageEvent(start, pageSize));
2819}
2820
2822{
2823 QCoreApplication::
2824 removePostedEvents(this, NextButtonListPageEvent::kEventType);
2825 return m_nextItemLoaded;
2826}
2827
2828QPoint MythUIButtonList::GetButtonPosition(int column, int row) const
2829{
2830 int x = m_contentsRect.x() +
2831 ((column - 1) * (m_itemWidth + m_itemHorizSpacing));
2832 int y = m_contentsRect.y() +
2833 ((row - 1) * (m_itemHeight + m_itemVertSpacing));
2834
2835 return {x, y};
2836}
2837
2839{
2840 m_itemsVisible = 0;
2841 m_rows = 0;
2842 m_columns = 0;
2843
2845 {
2846 int x = 0;
2847
2848 while (x <= m_contentsRect.width() - m_itemWidth)
2849 {
2851 ++m_columns;
2852 }
2853 }
2854
2855 if ((m_layout == LayoutVertical) || (m_layout == LayoutGrid))
2856 {
2857 int y = 0;
2858
2859 while (y <= m_contentsRect.height() - m_itemHeight)
2860 {
2862 ++m_rows;
2863 }
2864 }
2865
2866 if (m_rows <= 0)
2867 m_rows = 1;
2868
2869 if (m_columns <= 0)
2870 m_columns = 1;
2871
2873}
2874
2876{
2877 if (rect == m_contentsRect)
2878 return;
2879
2880 m_contentsRect = rect;
2881
2882 if (m_area.isValid())
2884 else if (m_parent)
2886 else
2887 m_contentsRect.CalculateArea(GetMythMainWindow()->GetUIScreenRect());
2888}
2889
2894 const QString &filename, QDomElement &element, bool showWarnings)
2895{
2896 if (element.tagName() == "buttonarea")
2897 SetButtonArea(parseRect(element));
2898 else if (element.tagName() == "layout")
2899 {
2900 QString layout = getFirstText(element).toLower();
2901
2902 if (layout == "grid")
2904 else if (layout == "horizontal")
2906 else
2908 }
2909 else if (element.tagName() == "arrange")
2910 {
2911 QString arrange = getFirstText(element).toLower();
2912
2913 if (arrange == "fill")
2915 else if (arrange == "spread")
2917 else if (arrange == "stack")
2919 else
2921
2922 }
2923 else if (element.tagName() == "align")
2924 {
2925 QString align = getFirstText(element).toLower();
2927 }
2928 else if (element.tagName() == "shadowalign")
2929 {
2930 QString align = getFirstText(element).toLower();
2932 }
2933 else if (element.tagName() == "scrollstyle")
2934 {
2935 QString layout = getFirstText(element).toLower();
2936
2937 if (layout == "center")
2939 else if (layout == "groupcenter")
2941 else if (layout == "free")
2943 }
2944 else if (element.tagName() == "wrapstyle")
2945 {
2946 QString wrapstyle = getFirstText(element).toLower();
2947
2948 if (wrapstyle == "captive")
2950 else if (wrapstyle == "none")
2952 else if (wrapstyle == "selection")
2954 else if (wrapstyle == "flowing")
2956 else if (wrapstyle == "items")
2958 }
2959 else if (element.tagName() == "showarrow")
2960 {
2961 m_showArrow = parseBool(element);
2962 }
2963 else if (element.tagName() == "showscrollbar")
2964 {
2965 m_showScrollBar = parseBool(element);
2966 }
2967 else if (element.tagName() == "spacing")
2968 {
2969 m_itemHorizSpacing = NormX(getFirstText(element).toInt());
2970 m_itemVertSpacing = NormY(getFirstText(element).toInt());
2971 }
2972 else if (element.tagName() == "drawfrombottom")
2973 {
2975
2977 m_defaultAlignment |= Qt::AlignBottom;
2978 }
2979 else if (element.tagName() == "shadowdrawfrombottom")
2980 {
2982
2984 m_shadowAlignment = m_shadowAlignment.value_or(0) | Qt::AlignBottom;
2985 }
2986 else if (element.tagName() == "searchposition")
2987 {
2988 m_searchPosition = parsePoint(element);
2989 }
2990 else if (element.tagName() == "triggerevent")
2991 {
2992 QString trigger = getFirstText(element);
2993 if (!trigger.isEmpty())
2994 {
2995 QString action = element.attribute("action", "");
2996 if (action.isEmpty())
2997 {
2998 m_actionRemap[trigger] = "";
2999 }
3000 else
3001 {
3002 QString context = element.attribute("context", "");
3003 QString keylist = MythMainWindow::GetKey(context, action);
3004 QStringList keys = keylist.split(',', Qt::SkipEmptyParts);
3005 if (!keys.empty())
3006 m_actionRemap[trigger] = keys[0];
3007 }
3008 }
3009 }
3010 else
3011 {
3012 return MythUIType::ParseElement(filename, element, showWarnings);
3013 }
3014
3015 return true;
3016}
3017
3021void MythUIButtonList::DrawSelf(MythPainter * /*p*/, int /*xoffset*/, int /*yoffset*/,
3022 int /*alphaMod*/, QRect /*clipRect*/)
3023{
3024 if (m_needsUpdate)
3025 {
3028 }
3029}
3030
3035{
3036 auto *lb = new MythUIButtonList(parent, objectName());
3037 lb->CopyFrom(this);
3038}
3039
3044{
3045 auto *lb = dynamic_cast<MythUIButtonList *>(base);
3046 if (!lb)
3047 return;
3048
3049 m_layout = lb->m_layout;
3050 m_arrange = lb->m_arrange;
3051 m_defaultAlignment = lb->m_defaultAlignment;
3052 m_shadowAlignment = lb->m_shadowAlignment;
3053
3054 m_contentsRect = lb->m_contentsRect;
3055
3056 m_itemHeight = lb->m_itemHeight;
3057 m_itemWidth = lb->m_itemWidth;
3058 m_itemHorizSpacing = lb->m_itemHorizSpacing;
3059 m_itemVertSpacing = lb->m_itemVertSpacing;
3060 m_itemsVisible = lb->m_itemsVisible;
3061 m_maxVisible = lb->m_maxVisible;
3062
3063 m_active = lb->m_active;
3064 m_showArrow = lb->m_showArrow;
3065 m_showScrollBar = lb->m_showScrollBar;
3066
3067 m_defaultDrawFromBottom = lb->m_defaultDrawFromBottom;
3068 m_shadowDrawFromBottom = lb->m_shadowDrawFromBottom;
3069
3070 m_scrollStyle = lb->m_scrollStyle;
3071 m_wrapStyle = lb->m_wrapStyle;
3072
3073 m_clearing = false;
3075
3076 m_searchPosition = lb->m_searchPosition;
3077 m_searchFields = lb->m_searchFields;
3078
3080
3081 m_upArrow = dynamic_cast<MythUIStateType *>(GetChild("upscrollarrow"));
3082 m_downArrow = dynamic_cast<MythUIStateType *>(GetChild("downscrollarrow"));
3083 m_scrollBar = dynamic_cast<MythUIScrollBar *>(GetChild("scrollbar"));
3084
3085 for (int i = 0; i < m_itemsVisible; ++i)
3086 {
3087 QString name = QString("buttonlist button %1").arg(i);
3088 DeleteChild(name);
3089 }
3090
3091 m_buttonList.clear();
3092
3093 m_actionRemap = lb->m_actionRemap;
3094
3095 m_initialized = false;
3096}
3097
3102{
3104}
3105
3106void MythUIButtonList::SetLCDTitles(const QString &title, const QString &columnList)
3107{
3108 m_lcdTitle = title;
3109 m_lcdColumns = columnList.split('|');
3110}
3111
3113{
3114 if (!m_hasFocus)
3115 return;
3116
3117 LCD *lcddev = LCD::Get();
3118
3119 if (lcddev == nullptr)
3120 return;
3121
3122 // Build a list of the menu items
3123 QList<LCDMenuItem> menuItems;
3124
3125 auto start = std::max(0, m_selPosition - lcddev->getLCDHeight());
3126 auto end = std::min(m_itemCount, start + (lcddev->getLCDHeight() * 2));
3127
3128 for (int r = start; r < end; ++r)
3129 {
3130 bool selected = r == GetCurrentPos();
3131
3134
3135 if (item->checkable())
3137
3138 QString text;
3139
3140 for (int x = 0; x < m_lcdColumns.count(); ++x)
3141 {
3142 if (!m_lcdColumns[x].isEmpty() && item->m_strings.contains(m_lcdColumns[x]))
3143 {
3144 // named text column
3145 TextProperties props = item->m_strings[m_lcdColumns[x]];
3146
3147 if (text.isEmpty())
3148 text = props.text;
3149 else
3150 text += " ~ " + props.text;
3151 }
3152 else
3153 {
3154 // default text column
3155 if (text.isEmpty())
3156 text = item->GetText();
3157 else
3158 text += " ~ " + item->GetText();
3159 }
3160 }
3161
3162 if (!text.isEmpty())
3163 menuItems.append(LCDMenuItem(selected, state, text));
3164 else
3165 menuItems.append(LCDMenuItem(selected, state, item->GetText()));
3166 }
3167
3168 if (!menuItems.isEmpty())
3169 lcddev->switchToMenu(menuItems, m_lcdTitle);
3170}
3171
3173{
3174 MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack");
3175
3176 auto *dlg = new SearchButtonListDialog(popupStack, "MythSearchListDialog", this, "");
3177
3178 if (dlg->Create())
3179 {
3180 if (m_searchPosition.x() != -2 || m_searchPosition.y() != -2)
3181 {
3182 int x = m_searchPosition.x();
3183 int y = m_searchPosition.y();
3184 QRect screenArea = GetMythMainWindow()->GetUIScreenRect();
3185 QRect dialogArea = dlg->GetArea();
3186
3187 if (x == -1)
3188 x = (screenArea.width() - dialogArea.width()) / 2;
3189
3190 if (y == -1)
3191 y = (screenArea.height() - dialogArea.height()) / 2;
3192
3193 dlg->SetPosition(x, y);
3194 }
3195
3196 popupStack->AddScreen(dlg);
3197 }
3198 else
3199 {
3200 delete dlg;
3201 }
3202}
3203
3204bool MythUIButtonList::Find(const QString &searchStr, bool startsWith)
3205{
3206 m_searchStr = searchStr;
3207 m_searchStartsWith = startsWith;
3208 return DoFind(false, true);
3209}
3210
3212{
3213 return DoFind(true, true);
3214}
3215
3217{
3218 return DoFind(true, false);
3219}
3220
3221bool MythUIButtonList::DoFind(bool doMove, bool searchForward)
3222{
3223 if (m_searchStr.isEmpty())
3224 return true;
3225
3226 if (GetCount() == 0)
3227 return false;
3228
3229 int startPos = GetCurrentPos();
3230 int currPos = startPos;
3231 bool found = false;
3232
3233 if (doMove)
3234 {
3235 if (searchForward)
3236 {
3237 ++currPos;
3238
3239 if (currPos >= GetCount())
3240 currPos = 0;
3241 }
3242 else
3243 {
3244 --currPos;
3245
3246 if (currPos < 0)
3247 currPos = GetCount() - 1;
3248 }
3249 }
3250
3251 while (true)
3252 {
3254
3255 if (found)
3256 {
3257 SetItemCurrent(currPos);
3258 return true;
3259 }
3260
3261 if (searchForward)
3262 {
3263 ++currPos;
3264
3265 if (currPos >= GetCount())
3266 currPos = 0;
3267 }
3268 else
3269 {
3270 --currPos;
3271
3272 if (currPos < 0)
3273 currPos = GetCount() - 1;
3274 }
3275
3276 if (startPos == currPos)
3277 break;
3278 }
3279
3280 return false;
3281}
3282
3284
3286 QString text, QString image,
3287 bool checkable, CheckState state,
3288 bool showArrow, int listPosition)
3289 : m_parent(lbtype), m_text(std::move(text)), m_imageFilename(std::move(image)),
3290 m_checkable(checkable), m_state(state), m_showArrow(showArrow)
3291{
3292 if (!lbtype)
3293 LOG(VB_GENERAL, LOG_ERR, "Cannot add a button to a non-existent list!");
3294
3295 if (state >= NotChecked)
3296 m_checkable = true;
3297
3298 if (m_parent)
3299 m_parent->InsertItem(this, listPosition);
3300}
3301
3303 const QString &text,
3304 QVariant data, int listPosition)
3305{
3306 if (!lbtype)
3307 LOG(VB_GENERAL, LOG_ERR, "Cannot add a button to a non-existent list!");
3308
3309 m_parent = lbtype;
3310 m_text = text;
3311 m_data = std::move(data);
3312
3313 m_image = nullptr;
3314
3315 m_checkable = false;
3317 m_showArrow = false;
3318 m_isVisible = false;
3319 m_enabled = true;
3320
3321 if (m_parent)
3322 m_parent->InsertItem(this, listPosition);
3323}
3324
3326{
3327 if (m_parent)
3328 m_parent->RemoveItem(this);
3329
3330 if (m_image)
3331 m_image->DecrRef();
3332
3333 QMap<QString, MythImage*>::iterator it;
3334 for (it = m_images.begin(); it != m_images.end(); ++it)
3335 {
3336 if (*it)
3337 (*it)->DecrRef();
3338 }
3339 m_images.clear();
3340}
3341
3342void MythUIButtonListItem::SetText(const QString &text, const QString &name,
3343 const QString &state)
3344{
3345 if (!name.isEmpty())
3346 {
3347 TextProperties textprop;
3348 textprop.text = text;
3349 textprop.state = state;
3350 m_strings.insert(name, textprop);
3351 }
3352 else
3353 {
3354 m_text = text;
3355 }
3356
3357 if (m_parent && m_isVisible)
3358 m_parent->Update();
3359}
3360
3362 const QString &state)
3363{
3364 InfoMap::const_iterator map_it = infoMap.begin();
3365
3366 while (map_it != infoMap.end())
3367 {
3368 TextProperties textprop;
3369 textprop.text = (*map_it);
3370 textprop.state = state;
3371 m_strings[map_it.key()] = textprop;
3372 ++map_it;
3373 }
3374
3375 if (m_parent && m_isVisible)
3376 m_parent->Update();
3377}
3378
3379void MythUIButtonListItem::SetTextFromMap(const QMap<QString, TextProperties> &stringMap)
3380{
3381 m_strings.clear();
3382 m_strings = stringMap;
3383}
3384
3386{
3387 m_textCb.fn = fn;
3388 m_textCb.data = data;
3389}
3390
3391QString MythUIButtonListItem::GetText(const QString &name) const
3392{
3393 if (name.isEmpty())
3394 return m_text;
3395 if (m_textCb.fn != nullptr)
3396 {
3397 QString result = m_textCb.fn(name, m_textCb.data);
3398 if (!result.isEmpty())
3399 return result;
3400 }
3401 if (m_strings.contains(name))
3402 return m_strings[name].text;
3403 return {};
3404}
3405
3407{
3408 if (name.isEmpty())
3409 return {m_text, ""};
3410 if (m_textCb.fn != nullptr)
3411 {
3412 QString result = m_textCb.fn(name, m_textCb.data);
3413 if (!result.isEmpty())
3414 return {result, ""};
3415 }
3416 if (m_strings.contains(name))
3417 return m_strings[name];
3418 return {};
3419}
3420
3421bool MythUIButtonListItem::FindText(const QString &searchStr, const QString &fieldList,
3422 bool startsWith) const
3423{
3424 if (fieldList.isEmpty())
3425 {
3426 if (startsWith)
3427 return m_text.startsWith(searchStr, Qt::CaseInsensitive);
3428 return m_text.contains(searchStr, Qt::CaseInsensitive);
3429 }
3430 if (fieldList == "**ALL**")
3431 {
3432 if (startsWith)
3433 {
3434 if (m_text.startsWith(searchStr, Qt::CaseInsensitive))
3435 return true;
3436 }
3437 else
3438 {
3439 if (m_text.contains(searchStr, Qt::CaseInsensitive))
3440 return true;
3441 }
3442
3443 QMap<QString, TextProperties>::const_iterator i = m_strings.constBegin();
3444
3445 while (i != m_strings.constEnd())
3446 {
3447 if (startsWith)
3448 {
3449 if (i.value().text.startsWith(searchStr, Qt::CaseInsensitive))
3450 return true;
3451 }
3452 else
3453 {
3454 if (i.value().text.contains(searchStr, Qt::CaseInsensitive))
3455 return true;
3456 }
3457
3458 ++i;
3459 }
3460 }
3461 else
3462 {
3463 QStringList fields = fieldList.split(',', Qt::SkipEmptyParts);
3464 for (int x = 0; x < fields.count(); ++x)
3465 {
3466 if (m_strings.contains(fields.at(x).trimmed()))
3467 {
3468 if (startsWith)
3469 {
3470 if (m_strings[fields.at(x)].text.startsWith(searchStr, Qt::CaseInsensitive))
3471 return true;
3472 }
3473 else
3474 {
3475 if (m_strings[fields.at(x)].text.contains(searchStr, Qt::CaseInsensitive))
3476 return true;
3477 }
3478 }
3479 }
3480 }
3481
3482 return false;
3483}
3484
3485void MythUIButtonListItem::SetFontState(const QString &state,
3486 const QString &name)
3487{
3488 if (!name.isEmpty())
3489 {
3490 if (m_strings.contains(name))
3491 m_strings[name].state = state;
3492 }
3493 else
3494 {
3496 }
3497
3498 if (m_parent && m_isVisible)
3499 m_parent->Update();
3500}
3501
3502void MythUIButtonListItem::SetImage(MythImage *image, const QString &name)
3503{
3504 if (image)
3505 image->IncrRef();
3506
3507 if (!name.isEmpty())
3508 {
3509 QMap<QString, MythImage*>::iterator it = m_images.find(name);
3510 if (it != m_images.end())
3511 {
3512 (*it)->DecrRef();
3513 if (image)
3514 *it = image;
3515 else
3516 m_images.erase(it);
3517 }
3518 else if (image)
3519 {
3520 m_images[name] = image;
3521 }
3522 }
3523 else
3524 {
3525 if (m_image)
3526 m_image->DecrRef();
3527 m_image = image;
3528 }
3529
3530 if (m_parent && m_isVisible)
3531 m_parent->Update();
3532}
3533
3535{
3536 m_imageFilenames.clear();
3537 m_imageFilenames = imageMap;
3538}
3539
3541{
3542 m_imageCb.fn = fn;
3543 m_imageCb.data = data;
3544}
3545
3547{
3548 if (!name.isEmpty())
3549 {
3550 QMap<QString, MythImage*>::iterator it = m_images.find(name);
3551 if (it != m_images.end())
3552 {
3553 (*it)->IncrRef();
3554 return (*it);
3555 }
3556 }
3557 else if (m_image)
3558 {
3559 m_image->IncrRef();
3560 return m_image;
3561 }
3562
3563 return nullptr;
3564}
3565
3567 const QString &filename, const QString &name, bool force_reload)
3568{
3569 bool do_update = force_reload;
3570
3571 if (!name.isEmpty())
3572 {
3573 InfoMap::iterator it = m_imageFilenames.find(name);
3574
3575 if (it == m_imageFilenames.end())
3576 {
3577 m_imageFilenames.insert(name, filename);
3578 do_update = true;
3579 }
3580 else if (*it != filename)
3581 {
3582 *it = filename;
3583 do_update = true;
3584 }
3585 }
3586 else if (m_imageFilename != filename)
3587 {
3589 do_update = true;
3590 }
3591
3592 if (m_parent && do_update && m_isVisible)
3593 m_parent->Update();
3594}
3595
3596QString MythUIButtonListItem::GetImageFilename(const QString &name) const
3597{
3598 if (name.isEmpty())
3599 return m_imageFilename;
3600
3601 if (m_imageCb.fn != nullptr)
3602 {
3603 QString result = m_imageCb.fn(name, m_imageCb.data);
3604 if (!result.isEmpty())
3605 return result;
3606 }
3607
3608 InfoMap::const_iterator it = m_imageFilenames.find(name);
3609
3610 if (it != m_imageFilenames.end())
3611 return *it;
3612
3613 return {};
3614}
3615
3616void MythUIButtonListItem::SetProgress1(int start, int total, int used)
3617{
3618 m_progress1.used = used;
3619 m_progress1.start = start;
3620 m_progress1.total = total;
3621
3622 if (m_parent && m_isVisible)
3623 m_parent->Update();
3624}
3625
3626void MythUIButtonListItem::SetProgress2(int start, int total, int used)
3627{
3628 m_progress2.used = used;
3629 m_progress2.start = start;
3630 m_progress2.total = total;
3631
3632 if (m_parent && m_isVisible)
3633 m_parent->Update();
3634}
3635
3636void MythUIButtonListItem::DisplayState(const QString &state,
3637 const QString &name)
3638{
3639 if (name.isEmpty())
3640 return;
3641
3642 bool do_update = false;
3643 InfoMap::iterator it = m_states.find(name);
3644
3645 if (it == m_states.end())
3646 {
3647 m_states.insert(name, state);
3648 do_update = true;
3649 }
3650 else if (*it != state)
3651 {
3652 *it = state;
3653 do_update = true;
3654 }
3655
3656 if (m_parent && do_update && m_isVisible)
3657 m_parent->Update();
3658}
3659
3661{
3662 m_states.clear();
3663 m_states = stateMap;
3664}
3665
3667{
3668 m_stateCb.fn = fn;
3669 m_stateCb.data = data;
3670}
3671
3672QString MythUIButtonListItem::GetState(const QString &name)
3673{
3674 if (name.isEmpty())
3675 return {};
3676 if (m_stateCb.fn != nullptr)
3677 {
3678 QString result = m_stateCb.fn(name, m_textCb.data);
3679 if (!result.isEmpty())
3680 return result;
3681 }
3682 if (m_states.contains(name))
3683 return m_states[name];
3684 return {};
3685}
3686
3688{
3689 return m_checkable;
3690}
3691
3693{
3694 return m_state;
3695}
3696
3698{
3699 return m_parent;
3700}
3701
3703{
3704 if (!m_checkable || m_state == state)
3705 return;
3706
3707 m_state = state;
3708
3709 if (m_parent && m_isVisible)
3710 m_parent->Update();
3711}
3712
3714{
3715 m_checkable = flag;
3716}
3717
3719{
3720 m_showArrow = flag;
3721}
3722
3724{
3725 return m_enabled;
3726}
3727
3729{
3730 m_enabled = flag;
3731}
3732
3734{
3735 m_data = std::move(data);
3736}
3737
3739{
3740 return m_data;
3741}
3742
3744{
3745 if (m_parent)
3746 return m_parent->MoveItemUpDown(this, flag);
3747 return false;
3748}
3749
3751{
3752 if (!buttontext)
3753 return;
3754
3755 buttontext->SetText(m_text);
3756 buttontext->SetFontState(m_fontState);
3757}
3758
3760{
3761 if (!buttonimage)
3762 return;
3763
3764 if (!m_imageFilename.isEmpty())
3765 {
3766 buttonimage->SetFilename(m_imageFilename);
3767 buttonimage->Load();
3768 }
3769 else if (m_image)
3770 {
3771 buttonimage->SetImage(m_image);
3772 }
3773}
3774
3776{
3777 if (!buttonarrow)
3778 return;
3779 buttonarrow->SetVisible(m_showArrow);
3780}
3781
3783{
3784 if (!buttoncheck)
3785 return;
3786
3787 buttoncheck->SetVisible(m_checkable);
3788
3789 if (!m_checkable)
3790 return;
3791
3792 if (m_state == NotChecked)
3793 buttoncheck->DisplayState(MythUIStateType::Off);
3794 else if (m_state == HalfChecked)
3795 buttoncheck->DisplayState(MythUIStateType::Half);
3796 else
3797 buttoncheck->DisplayState(MythUIStateType::Full);
3798}
3799
3801{
3802 if (!buttonprogress)
3803 return;
3804
3806}
3807
3809{
3810 if (!buttonprogress)
3811 return;
3812
3814}
3815
3817 const TextProperties& textprop)
3818{
3819 if (!text)
3820 return;
3821
3822 QString newText = text->GetTemplateText();
3823
3824 static const QRegularExpression re {R"(%(([^\|%]+)?\||\|(.))?([\w#]+)(\|(.+?))?%)",
3825 QRegularExpression::DotMatchesEverythingOption};
3826
3827 if (!newText.isEmpty() && newText.contains(re))
3828 {
3829 QString tempString = newText;
3830
3831 QRegularExpressionMatchIterator i = re.globalMatch(newText);
3832 while (i.hasNext()) {
3833 QRegularExpressionMatch match = i.next();
3834 QString key = match.captured(4).toLower().trimmed();
3835 QString replacement;
3836 QString value = GetText(key);
3837
3838 if (!value.isEmpty())
3839 {
3840 replacement = QString("%1%2%3%4")
3841 .arg(match.captured(2),
3842 match.captured(3),
3843 value,
3844 match.captured(6));
3845 }
3846
3847 tempString.replace(match.captured(0), replacement);
3848 }
3849
3850 newText = tempString;
3851 }
3852 else
3853 {
3854 newText = textprop.text;
3855 }
3856
3857 if (newText.isEmpty())
3858 text->Reset();
3859 else
3860 text->SetText(newText);
3861
3862 text->SetFontState(textprop.state.isEmpty() ? m_fontState : textprop.state);
3863}
3864
3866{
3867 if (!image)
3868 return;
3869
3870 if (!filename.isEmpty())
3871 {
3872 image->SetFilename(filename);
3873 image->Load();
3874 }
3875 else
3876 {
3877 image->Reset();
3878 }
3879}
3880
3882{
3883 if (!uiimage)
3884 return;
3885
3886 if (image)
3887 uiimage->SetImage(image);
3888 else
3889 uiimage->Reset();
3890}
3891
3893{
3894 if (!statetype)
3895 return;
3896
3897 if (!statetype->DisplayState(name))
3898 statetype->Reset();
3899}
3900
3902 bool selected)
3903{
3904 if (!m_parent)
3905 return;
3906
3907 m_parent->ItemVisible(this);
3908 m_isVisible = true;
3909
3910 QString state;
3911
3912 if (!m_parent->IsEnabled())
3913 state = "disabled";
3914 else if (!m_enabled)
3915 {
3916 state = m_parent->m_active ? "disabledactive" : "disabledinactive";
3917 }
3918 else if (selected)
3919 {
3920 button->MoveToTop();
3921 state = m_parent->m_active ? "selectedactive" : "selectedinactive";
3922 }
3923 else
3924 {
3925 state = m_parent->m_active ? "active" : "inactive";
3926 }
3927
3928 if (m_parent->IsShadowing())
3929 {
3930 if (state == "inactive" && button->GetState("shadow"))
3931 state = "shadow";
3932 else if (state == "selectedinactive" &&
3933 button->GetState("selectedshadow"))
3934 state = "selectedshadow";
3935 }
3936
3937 // Begin compatibility code
3938 // Attempt to fallback if the theme is missing certain states
3939 if (state == "disabled" && !button->GetState(state))
3940 {
3941 LOG(VB_GUI, LOG_WARNING, "Theme Error: Missing buttonlist state: disabled");
3942 state = "inactive";
3943 }
3944
3945 if (state == "inactive" && !button->GetState(state))
3946 {
3947 LOG(VB_GUI, LOG_WARNING, "Theme Error: Missing buttonlist state: inactive");
3948 state = "active";
3949 }
3950 // End compatibility code
3951
3952 auto *buttonstate = dynamic_cast<MythUIGroup *>(button->GetState(state));
3953 if (!buttonstate)
3954 {
3955 LOG(VB_GENERAL, LOG_CRIT, QString("Theme Error: Missing buttonlist state: %1")
3956 .arg(state));
3957 return;
3958 }
3959
3960 buttonstate->Reset();
3961
3962 QList<MythUIType *> descendants = buttonstate->GetAllDescendants();
3963 for (MythUIType *obj : std::as_const(descendants))
3964 {
3965 QString name = obj->objectName();
3966 if (name == "buttontext")
3967 DoButtonText(dynamic_cast<MythUIText *>(obj));
3968 else if (name == "buttonimage")
3969 DoButtonImage(dynamic_cast<MythUIImage *>(obj));
3970 else if (name == "buttonarrow")
3971 DoButtonArrow(dynamic_cast<MythUIImage *>(obj));
3972 else if (name == "buttoncheck")
3973 DoButtonCheck(dynamic_cast<MythUIStateType *>(obj));
3974 else if (name == "buttonprogress1")
3975 DoButtonProgress1(dynamic_cast<MythUIProgressBar *>(obj));
3976 else if (name == "buttonprogress2")
3977 DoButtonProgress2(dynamic_cast<MythUIProgressBar *>(obj));
3978
3979 TextProperties textprop = GetTextProp(name);
3980 if (!textprop.text.isEmpty())
3981 DoButtonLookupText(dynamic_cast<MythUIText *>(obj), textprop);
3982
3983 QString filename = GetImageFilename(name);
3984 if (!filename.isEmpty())
3985 DoButtonLookupFilename (dynamic_cast<MythUIImage *>(obj), filename);
3986
3987 if (m_images.contains(name))
3988 DoButtonLookupImage(dynamic_cast<MythUIImage *>(obj), m_images[name]);
3989
3990 QString luState = GetState(name);
3991 if (!luState.isEmpty())
3992 DoButtonLookupState(dynamic_cast<MythUIStateType *>(obj), luState);
3993 }
3994
3995 // There is no need to check the return value here, since we already
3996 // checked that the state exists with GetState() earlier
3997 button->DisplayState(state);
3998}
3999
4000//---------------------------------------------------------
4001// SearchButtonListDialog
4002//---------------------------------------------------------
4004{
4005 if (!CopyWindowFromBase("MythSearchListDialog", this))
4006 return false;
4007
4008 bool err = false;
4009 UIUtilE::Assign(this, m_searchEdit, "searchedit", &err);
4010 UIUtilE::Assign(this, m_prevButton, "prevbutton", &err);
4011 UIUtilE::Assign(this, m_nextButton, "nextbutton", &err);
4012 UIUtilW::Assign(this, m_searchState, "searchstate");
4013
4014 if (err)
4015 {
4016 LOG(VB_GENERAL, LOG_ERR, "Cannot load screen 'MythSearchListDialog'");
4017 return false;
4018 }
4019
4021
4025
4027
4028 return true;
4029}
4030
4032{
4033 if (GetFocusWidget() && GetFocusWidget()->keyPressEvent(event))
4034 return true;
4035
4036 QStringList actions;
4037 bool handled = GetMythMainWindow()->TranslateKeyPress("Global", event, actions, false);
4038
4039 for (int i = 0; i < actions.size() && !handled; ++i)
4040 {
4041 const QString& action = actions[i];
4042 handled = true;
4043
4044 if (action == "0")
4045 {
4047 searchChanged();
4048 }
4049 else
4050 {
4051 handled = false;
4052 }
4053 }
4054
4055 if (!handled && MythScreenType::keyPressEvent(event))
4056 handled = true;
4057
4058 return handled;
4059}
4060
4062{
4064
4065 if (m_searchState)
4066 m_searchState->DisplayState(found ? "found" : "notfound");
4067}
4068
4070{
4071 bool found = m_parentList->FindNext();
4072
4073 if (m_searchState)
4074 m_searchState->DisplayState(found ? "found" : "notfound");
4075}
4076
4078{
4079 bool found = m_parentList->FindPrev();
4080
4081 if (m_searchState)
4082 m_searchState->DisplayState(found ? "found" : "notfound");
4083}
4084
4086{
4088 return;
4089
4090 int maximum = (m_itemCount <= m_itemsVisible) ? 0 : m_itemCount;
4091 m_scrollBar->SetMaximum(maximum);
4095}
Definition: lcddevice.h:170
static LCD * Get(void)
Definition: lcddevice.cpp:69
void switchToMenu(QList< LCDMenuItem > &menuItems, const QString &app_name="", bool popMenu=true)
Definition: lcddevice.cpp:590
int getLCDHeight(void) const
Definition: lcddevice.h:292
A custom event that represents a mouse gesture.
Definition: mythgesture.h:40
Gesture GetGesture() const
Definition: mythgesture.h:85
int DecrRef(void) override
Decrements reference count and deletes on 0.
Definition: mythimage.cpp:52
int IncrRef(void) override
Increments reference count.
Definition: mythimage.cpp:44
bool TranslateKeyPress(const QString &Context, QKeyEvent *Event, QStringList &Actions, bool AllowJumps=true)
Get a list of actions for a keypress in the given context.
MythScreenStack * GetStack(const QString &Stackname)
static QString GetKey(const QString &Context, const QString &Action)
bool isValid(void) const
Definition: mythrect.h:102
Wrapper around QRect allowing us to handle percentage and other relative values for areas in mythui.
Definition: mythrect.h:18
MythPoint topLeft(void) const
Definition: mythrect.cpp:288
void setY(const QString &sY)
Definition: mythrect.cpp:256
void setX(const QString &sX)
Definition: mythrect.cpp:246
void setWidth(const QString &sWidth)
Definition: mythrect.cpp:266
void setHeight(const QString &sHeight)
Definition: mythrect.cpp:277
void CalculateArea(QRect parentArea)
Definition: mythrect.cpp:64
virtual void AddScreen(MythScreenType *screen, bool allowFade=true)
void BuildFocusList(void)
MythUIType * GetFocusWidget(void) const
bool keyPressEvent(QKeyEvent *event) override
Key event handler.
void DoButtonProgress2(MythUIProgressBar *buttonprogress) const
void setEnabled(bool flag)
static void DoButtonLookupState(MythUIStateType *statetype, const QString &name)
void SetFontState(const QString &state, const QString &name="")
void SetTextCb(muibCbFn fn, void *data)
void SetData(QVariant data)
static void DoButtonLookupImage(MythUIImage *uiimage, MythImage *image)
void SetProgress1(int start, int total, int used)
QString GetState(const QString &name)
MythImage * GetImage(const QString &name="")
Gets a MythImage which has been assigned to this button item, as with SetImage() it should only be us...
bool FindText(const QString &searchStr, const QString &fieldList="**ALL**", bool startsWith=false) const
void setCheckable(bool flag)
void DisplayState(const QString &state, const QString &name)
void setDrawArrow(bool flag)
void DoButtonLookupText(MythUIText *text, const TextProperties &textprop)
void DoButtonArrow(MythUIImage *buttonarrow) const
void DoButtonProgress1(MythUIProgressBar *buttonprogress) const
TextProperties GetTextProp(const QString &name="") const
void SetProgress2(int start, int total, int used)
QMap< QString, MythImage * > m_images
bool MoveUpDown(bool flag)
void SetTextFromMap(const InfoMap &infoMap, const QString &state="")
MythUIButtonListItem(MythUIButtonList *lbtype, QString text, QString image="", bool checkable=false, CheckState state=CantCheck, bool showArrow=false, int listPosition=-1)
void SetImage(MythImage *image, const QString &name="")
Sets an image directly, should only be used in special circumstances since it bypasses the cache.
void DoButtonImage(MythUIImage *buttonimage)
void SetImageCb(muibCbFn fn, void *data)
CheckState state() const
void DoButtonCheck(MythUIStateType *buttoncheck)
void DoButtonText(MythUIText *buttontext)
virtual void SetToRealButton(MythUIStateType *button, bool selected)
void setChecked(CheckState state)
MythUIButtonList * m_parent
MythUIButtonList * parent() const
void SetStateCb(muibCbFn fn, void *data)
void SetStatesFromMap(const InfoMap &stateMap)
QString GetImageFilename(const QString &name="") const
QMap< QString, TextProperties > m_strings
void SetImageFromMap(const InfoMap &imageMap)
static void DoButtonLookupFilename(MythUIImage *image, const QString &filename)
QString GetText(const QString &name="") const
void SetText(const QString &text, const QString &name="", const QString &state="")
List widget, displays list items in a variety of themeable arrangements and can trigger signals when ...
QHash< QString, QString > m_actionRemap
bool DistributeButtons(void)
bool ParseElement(const QString &filename, QDomElement &element, bool showWarnings) override
Parse the xml definition of this widget setting the state of the object accordingly.
virtual bool MoveDown(MovementUnit unit=MoveItem, uint amount=0)
bool MoveItemUpDown(MythUIButtonListItem *item, bool up)
QMap< int, MythUIButtonListItem * > m_buttonToItem
void FindEnabledDown(MovementUnit unit)
If the current item is not enabled, find the next enabled one.
MythUIStateType * m_downArrow
MythUIStateType * m_buttontemplate
void SetLCDTitles(const QString &title, const QString &columnList="")
virtual QString GetValue() const
QStringList m_lcdColumns
MythUIButtonListItem * GetItemCurrent() const
void itemVisible(MythUIButtonListItem *item)
void SetItemCurrent(MythUIButtonListItem *item)
void SetAllChecked(MythUIButtonListItem::CheckState state)
void CalculateArrowStates(void)
MythUIScrollBar * m_scrollBar
virtual void CalculateVisibleItems(void)
MythUIButtonList(MythUIType *parent, const QString &name, QString shadow="")
std::optional< int > m_shadowAlignment
void InsertItem(MythUIButtonListItem *item, int listPosition=-1)
MythUIStateType * m_upArrow
MythUIButtonListItem * GetItemFirst() const
void RemoveItem(MythUIButtonListItem *item)
void ItemVisible(MythUIButtonListItem *item)
virtual QPoint GetButtonPosition(int column, int row) const
virtual int GetIntValue() const
int minButtonHeight(const MythRect &area)
void SetScrollBarPosition(void)
void Reset() override
Reset the widget to it's original state, should not reset changes made by the theme.
QVector< MythUIStateType * > m_buttonList
bool IsShadowing(void)
MythPoint m_searchPosition
int GetItemPos(MythUIButtonListItem *item) const
ScrollStyle m_scrollStyle
MythUIButtonListItem * GetItemByData(const QVariant &data)
bool DoFind(bool doMove, bool searchForward)
virtual bool MoveUp(MovementUnit unit=MoveItem, uint amount=0)
int minButtonWidth(const MythRect &area)
void itemLoaded(MythUIButtonListItem *item)
void CreateCopy(MythUIType *parent) override
Copy the state of this widget to the one given, it must be of the same type.
void SetActive(bool active)
void FindEnabledUp(MovementUnit unit)
bool DistributeRow(int &first_button, int &last_button, int &first_item, int &last_item, int &selected_column, int &skip_cols, bool grow_left, bool grow_right, int **col_widths, int &row_height, int total_height, int split_height, int &col_cnt, bool &wrapped)
bool DistributeCols(int &first_button, int &last_button, int &first_item, int &last_item, int &selected_column, int &selected_row, int &skip_cols, int **col_widths, QList< int > &row_heights, int &top_height, int &bottom_height, bool &wrapped)
int GetCurrentPos() const
bool keyPressEvent(QKeyEvent *event) override
Key event handler.
void itemClicked(MythUIButtonListItem *item)
MythUIButtonListItem * GetItemAt(int pos) const
MythRect GetButtonArea(void) const
~MythUIButtonList() override
std::optional< bool > m_shadowDrawFromBottom
MythUIButtonListItem * GetItemNext(MythUIButtonListItem *item) const
void SetValueByData(const QVariant &data)
void InitButton(int itemIdx, MythUIStateType *&realButton, MythUIButtonListItem *&buttonItem)
QList< MythUIButtonListItem * > m_itemList
void CalculateButtonPositions(void)
void SetButtonArea(const MythRect &rect)
void LoadInBackground(int start=0, int pageSize=20)
bool MoveToNamedPosition(const QString &position_name)
QVariant GetDataValue() const
void Finalize(void) override
Perform any post-xml parsing initialisation tasks.
ArrangeType m_arrange
void itemSelected(MythUIButtonListItem *item)
void customEvent(QEvent *event) override
bool Find(const QString &searchStr, bool startsWith=false)
void DrawSelf(MythPainter *p, int xoffset, int yoffset, int alphaMod, QRect clipRect) override
bool gestureEvent(MythGestureEvent *event) override
Mouse click/movement handler, receives mouse gesture events from the QCoreApplication event loop.
MythUIGroup * PrepareButton(int buttonIdx, int itemIdx, int &selectedIdx, int &button_shift)
void CopyFrom(MythUIType *base) override
Copy this widgets state from another.
void Clicked()
Create a group of widgets.
Definition: mythuigroup.h:12
Image widget, displays a single image or multiple images in sequence.
Definition: mythuiimage.h:98
bool Load(bool allowLoadInBackground=true, bool forceStat=false)
Load the image(s), wraps ImageLoader::LoadImage()
void SetFilename(const QString &filename)
Must be followed by a call to Load() to load the image.
void SetImage(MythImage *img)
Should not be used unless absolutely necessary since it bypasses the image caching and threaded loade...
void Reset(void) override
Reset the image back to the default defined in the theme.
Progress bar widget.
void Set(int start, int total, int used)
Scroll bar widget.
void SetMaximum(int value)
void SetPageStep(int value)
void SetSliderPosition(int value)
This widget is used for grouping other widgets for display when a particular named state is called.
MythUIType * GetState(const QString &name)
MythUIType * GetCurrentState()
void Reset(void) override
Reset the widget to it's original state, should not reset changes made by the theme.
bool DisplayState(const QString &name)
QString GetText(void) const
void SetText(const QString &text, bool moveCursor=true)
void valueChanged()
All purpose text widget, displays a text string.
Definition: mythuitext.h:29
void Reset(void) override
Reset the widget to it's original state, should not reset changes made by the theme.
Definition: mythuitext.cpp:65
QString GetTemplateText(void) const
Definition: mythuitext.h:49
void SetFontState(const QString &state)
Definition: mythuitext.cpp:202
virtual void SetText(const QString &text)
Definition: mythuitext.cpp:115
The base class on which all widgets and screens are based.
Definition: mythuitype.h:86
bool IsEnabled(void) const
Definition: mythuitype.h:119
bool m_enableInitiator
Definition: mythuitype.h:267
void SetCanTakeFocus(bool set=true)
Set whether this widget can take focus.
Definition: mythuitype.cpp:362
bool m_initiator
Definition: mythuitype.h:268
void TakingFocus(void)
void RequestUpdate(void)
virtual void SetVisible(bool visible)
QString GetXMLLocation(void) const
Definition: mythuitype.h:182
void Disabling(void)
MythPoint m_minSize
Definition: mythuitype.h:279
virtual void CopyFrom(MythUIType *base)
Copy this widgets state from another.
virtual void Finalize(void)
Perform any post-xml parsing initialisation tasks.
MythUIType * GetChildAt(QPoint p, bool recursive=true, bool focusable=true) const
Return the first MythUIType at the given coordinates.
Definition: mythuitype.cpp:241
void SetRedraw(void)
Definition: mythuitype.cpp:313
void Enabling(void)
virtual void SetMinArea(const MythRect &rect)
Set the minimum area based on the given size.
Definition: mythuitype.cpp:820
static int NormY(int height)
virtual MythRect GetArea(void) const
If the object has a minimum area defined, return it, other wise return the default area.
Definition: mythuitype.cpp:885
MythUIType * m_parent
Definition: mythuitype.h:297
virtual MythRect GetFullArea(void) const
Definition: mythuitype.cpp:893
bool m_hasFocus
Definition: mythuitype.h:264
void SetPosition(int x, int y)
Convenience method, calls SetPosition(const MythPoint&) Override that instead to change functionality...
Definition: mythuitype.cpp:533
MythUIType * GetChild(const QString &name) const
Get a named child of this UIType.
Definition: mythuitype.cpp:138
bool MoveToTop(void)
void DeleteChild(const QString &name)
Delete a named child of this UIType.
Definition: mythuitype.cpp:153
void DependChanged(bool isDefault)
virtual void Reset(void)
Reset the widget to it's original state, should not reset changes made by the theme.
Definition: mythuitype.cpp:73
virtual bool ParseElement(const QString &filename, QDomElement &element, bool showWarnings)
Parse the xml definition of this widget setting the state of the object accordingly.
static int NormX(int width)
MythRect m_area
Definition: mythuitype.h:277
void LosingFocus(void)
NextButtonListPageEvent(int start, int pageSize)
static const Type kEventType
MythUIStateType * m_searchState
MythUIButton * m_nextButton
bool Create(void) override
MythUIButtonList * m_parentList
MythUITextEdit * m_searchEdit
MythUIButton * m_prevButton
bool keyPressEvent(QKeyEvent *event) override
Key event handler.
static MythRect parseRect(const QString &text, bool normalize=true)
static MythPoint parsePoint(const QString &text, bool normalize=true)
static int parseAlignment(const QString &text)
static bool CopyWindowFromBase(const QString &windowname, MythScreenType *win)
static QString getFirstText(QDomElement &element)
static bool parseBool(const QString &text)
unsigned int uint
Definition: compat.h:68
CHECKED_STATE
Definition: lcddevice.h:17
@ NOTCHECKABLE
Definition: lcddevice.h:17
@ CHECKED
Definition: lcddevice.h:17
@ UNCHECKED
Definition: lcddevice.h:17
A C++ ripoff of the stroke library for MythTV.
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
MythMainWindow * GetMythMainWindow(void)
QHash< QString, QString > InfoMap
Definition: mythtypes.h:15
QString(*)(const QString &name, void *data) muibCbFn
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:15
STL namespace.
static bool Assign(ContainerType *container, UIType *&item, const QString &name, bool *err=nullptr)
Definition: mythuiutils.h:27