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 {
396 end = first_item;
397 }
398 else
399 {
400 // Are we allowed to wrap when we run out of items?
401 if (m_wrapStyle == WrapItems &&
402 (hsplit || m_scrollStyle != ScrollFree) &&
403 last_item + 1 == m_itemCount)
404 {
405 last_item = -1;
406 wrapped = true;
407 end = first_item;
408 }
409 else
410 {
411 end = m_itemCount;
412 }
413 }
414
415 if (last_item + 1 < end)
416 {
417 // Allocate next button to the right.
418 buttonstate = PrepareButton(last_button + 1, last_item + 1,
419 selectedIdx, button_shift);
420
421 if (buttonstate == nullptr)
422 continue;
423
424 width = minButtonWidth(buttonstate->GetArea());
425
426 // For grids, use the widest button in a column
427 if (*col_widths && width < (*col_widths)[col_idx])
428 width = (*col_widths)[col_idx];
429
430 // Does the button fit?
431 if ((hsplit ? right_width : left_width + right_width) +
432 m_itemHorizSpacing + width > max_width)
433 {
434 int total = hsplit ? right_width : left_width + right_width;
435 LOG(VB_GUI, LOG_DEBUG,
436 QString("button on right would exceed width: "
437 "%1+(%2)+%3 == %4 which is > %5")
438 .arg(total).arg(m_itemHorizSpacing).arg(width)
439 .arg(total + m_itemHorizSpacing + width)
440 .arg(max_width));
441 }
442 else
443 {
444 added = true;
445 ++col_cnt;
446 ++last_button;
447 ++last_item;
448 ++col_idx;
449 right_width += m_itemHorizSpacing + width;
450 int height = minButtonHeight(buttonstate->GetArea());
451
452 row_height = std::max(row_height, height);
453
454 LOG(VB_GUI, LOG_DEBUG,
455 QString("Added button item %1 "
456 "R.width %2 height %3 total width %4+%5"
457 " (max %6)")
458 .arg(last_item).arg(width).arg(height)
459 .arg(left_width).arg(right_width).arg(max_width));
460 }
461 }
462 else
463 {
464 underflow = true;
465 }
466 }
467
468 // If a grid, maintain same number of columns on each row.
469 if (grow_left && col_cnt < m_columns)
470 {
471 if (wrapped)
472 {
473 end = last_item + 1;
474 }
475 else
476 {
477 // Are we allowed to wrap when we run out of items?
478 if (m_wrapStyle == WrapItems &&
479 (hsplit || m_scrollStyle != ScrollFree) &&
480 first_item == 0)
481 {
482 first_item = m_itemCount;
483 wrapped = true;
484 end = last_item + 1;
485 }
486 else
487 {
488 end = 0;
489 }
490 }
491
492 if (first_item > end)
493 {
494 buttonstate = PrepareButton(first_button - 1, first_item - 1,
495 selectedIdx, button_shift);
496
497 if (buttonstate == nullptr)
498 continue;
499
500 width = minButtonWidth(buttonstate->GetArea());
501
502 // For grids, use the widest button in a column
503 if (*col_widths && width < (*col_widths)[col_idx])
504 width = (*col_widths)[col_idx];
505
506 // Does the button fit?
507 if ((hsplit ? left_width : left_width + right_width) +
508 m_itemHorizSpacing + width > max_width)
509 {
510 int total = hsplit ? left_width : left_width + right_width;
511 LOG(VB_GUI, LOG_DEBUG,
512 QString("button on left would exceed width: "
513 "%1+(%2)+%3 == %4 which is > %5")
514 .arg(total).arg(m_itemHorizSpacing).arg(width)
515 .arg(total + m_itemHorizSpacing + width)
516 .arg(max_width));
517 }
518 else
519 {
520 added = true;
521 --first_button;
522 --first_item;
523 --col_idx;
524 ++col_cnt;
525 left_width += m_itemHorizSpacing + width;
526 int height = minButtonHeight(buttonstate->GetArea());
527
528 row_height = std::max(row_height, height);
529
530 LOG(VB_GUI, LOG_DEBUG,
531 QString("Added button item %1 "
532 "L.width %2 height %3 total width %4+%5"
533 " (max %6)")
534 .arg(first_item).arg(width).arg(height)
535 .arg(left_width).arg(right_width).arg(max_width));
536 }
537 }
538 else
539 {
540 underflow = true;
541 if (m_layout == LayoutGrid)
542 skip_cols = m_columns - col_cnt;
543 }
544 }
545 }
546
547 /*
548 * If total_height == 0, then this is the first row, so allow any height.
549 * Otherwise, If adding a button to a column would exceed the
550 * parent height, abort
551 */
552 if (total_height > 0 &&
553 ((vsplit ? split_height : total_height) +
554 m_itemVertSpacing + row_height > max_height))
555 {
556 LOG(VB_GUI, LOG_DEBUG,
557 QString("%1 Height exceeded %2 + (%3) + %4 = %5 which is > %6")
558 .arg(vsplit ? "Centering" : "Total")
559 .arg(split_height).arg(m_itemVertSpacing).arg(row_height)
560 .arg(split_height + m_itemVertSpacing + row_height)
561 .arg(max_height));
562 first_button = initial_first_button + button_shift;
563 last_button = initial_last_button + button_shift;
564 first_item = initial_first_item;
565 last_item = initial_last_item;
566 return false;
567 }
568
569 if (*col_widths == nullptr)
570 {
571 /*
572 * Allocate array to hold columns widths, now that we know
573 * how many columns there are.
574 */
575 *col_widths = new int[static_cast<size_t>(col_cnt)];
576
577 for (col_idx = 0; col_idx < col_cnt; ++col_idx)
578 (*col_widths)[col_idx] = 0;
579
580 }
581
582 // Adjust for insertions on the front.
583 first_button += button_shift;
584 last_button += button_shift;
585
586 // It fits, so so note max column widths
587 MythUIStateType *realButton = nullptr;
588 int buttonIdx = 0;
589
590 if (grow_left)
591 {
592 begin = first_button;
593 end = first_button + col_cnt;
594 }
595 else
596 {
597 end = last_button + 1;
598 begin = end - col_cnt;
599 }
600
601 for (buttonIdx = begin, col_idx = 0;
602 buttonIdx < end; ++buttonIdx, ++col_idx)
603 {
604 realButton = m_buttonList[buttonIdx];
605 buttonstate = dynamic_cast<MythUIGroup *>
606 (realButton->GetCurrentState());
607 if (!buttonstate)
608 break;
609 width = minButtonWidth(buttonstate->GetArea());
610
611 (*col_widths)[col_idx] = std::max((*col_widths)[col_idx], width);
612
613 // Make note of which column has the selected button
614 if (selectedIdx == buttonIdx)
615 selected_column = col_idx;
616 }
617
618 /*
619 An underflow indicates we ran out of items, not that the
620 buttons did not fit on the row.
621 */
622 if (total_height && underflow && col_cnt < m_columns)
623 col_cnt = m_columns;
624
625 return true;
626}
627
628/*
629 * Dynamically layout columns
630 */
631bool MythUIButtonList::DistributeCols(int &first_button, int &last_button,
632 int &first_item, int &last_item,
633 int &selected_column, int &selected_row,
634 int &skip_cols, int **col_widths,
635 QList<int> & row_heights,
636 int &top_height, int &bottom_height,
637 bool &wrapped)
638{
639 int col_cnt = 0;
640 int height = 0;
641 int end = 0;
642 bool added = true;
643
644 while (added)
645 {
646 added = false;
647
648 if (wrapped)
649 {
650 end = first_item;
651 }
652 else
653 {
654 // Are we allowed to wrap when we run out of items?
655 if (m_wrapStyle == WrapItems &&
658 last_item + 1 == m_itemCount)
659 {
660 last_item = -1;
661 wrapped = true;
662 end = first_item;
663 }
664 else
665 {
666 end = m_itemCount;
667 }
668 }
669
670 if (last_item + 1 < end)
671 {
672 // Does another row fit?
673 if (DistributeRow(first_button, ++last_button,
674 first_item, ++last_item, selected_column,
675 skip_cols, false, true, col_widths, height,
676 top_height + bottom_height, bottom_height,
677 col_cnt, wrapped))
678 {
679 if (col_cnt < m_columns)
680 return false; // Need to try again with fewer cols
681
682 if (selected_row == -1 && selected_column != -1)
683 selected_row = row_heights.size();
684
685 row_heights.push_back(height);
686 bottom_height += (height + m_itemVertSpacing);
687 added = true;
688 }
689 else
690 {
691 --last_button;
692 --last_item;
693 }
694 }
695
696 if (wrapped)
697 {
698 end = last_item + 1;
699 }
700 else
701 {
702 // Are we allowed to wrap when we run out of items?
703 if (m_wrapStyle == WrapItems &&
706 first_item == 0)
707 {
708 first_item = m_itemCount;
709 wrapped = true;
710 end = last_item + 1;
711 }
712 else
713 {
714 end = 0;
715 }
716 }
717
718 if (first_item > end)
719 {
720 // Can we insert another row?
721 if (DistributeRow(--first_button, last_button,
722 --first_item, last_item, selected_column,
723 skip_cols, true, false, col_widths, height,
724 top_height + bottom_height, top_height,
725 col_cnt, wrapped))
726 {
727 if (col_cnt < m_columns)
728 return false; // Need to try again with fewer cols
729
730 if (selected_row == -1 && selected_column != -1)
731 selected_row = row_heights.size();
732 else if (selected_row != -1)
733 ++selected_row;
734
735 row_heights.push_front(height);
736 top_height += (height + m_itemVertSpacing);
737 added = true;
738 }
739 else
740 {
741 ++first_button;
742 ++first_item;
743 }
744 }
745 }
746
747 return true;
748}
749
750/*
751 * Dynamically layout as many buttons as will fit in the area.
752 */
754{
755 int first_button = 0;
756 int last_button = 0;
757 int start_button = 0;
758 int start_item = m_selPosition;
759 int first_item = 0;
760 int last_item = 0;
761 int skip_cols = 0;
762 int *col_widths = nullptr;
763 int col_cnt = 0;
764 int selected_column = -1;
765 int selected_row = -1;
766 bool wrapped = false;
767 bool grow_left = true;
768 int height = 0;
769 int top_height = 0;
770 int bottom_height = 0;
771
772 QList<int> row_heights;
773
774 int alignment = IsShadowing() && m_shadowAlignment ?
776
777 LOG(VB_GUI, LOG_DEBUG, QString("DistributeButtons: "
778 "selected item %1 total items %2")
779 .arg(start_item).arg(m_itemCount));
780
781 // if there are no items to show make sure all the buttons are made invisible
782 if (m_itemCount == 0)
783 {
784 for (int i = 0; i < m_buttonList.count(); ++i)
785 {
786 if (m_buttonList[i])
787 m_buttonList[i]->SetVisible(false);
788 }
789
790 return false;
791 }
792
793 /*
794 * Try fewer and fewer columns until each row can fit the same
795 * number of columns.
796 */
797 for (m_columns = m_itemCount; m_columns > 0;)
798 {
799 first_item = last_item = start_item;
800
801 /*
802 * Drawing starts at start_button, and radiates from there.
803 * Attempt to pick a start_button which will minimize the need for new
804 * button allocations.
805 */
806 switch (m_scrollStyle)
807 {
808 case ScrollCenter:
810 start_button = std::max((m_maxVisible / 2) - 1, 0);
811 break;
812 case ScrollFree:
813
814 if (m_layout == LayoutGrid)
815 {
816 start_button = 0;
817 first_item = last_item = 0;
818 grow_left = false;
819 }
820 else if (!m_buttonList.empty())
821 {
822 if (m_itemCount - m_selPosition - 1 <
823 (m_buttonList.size() / 2))
824 {
825 start_button = m_buttonList.size() -
827 }
828 else if (m_selPosition >
829 (m_buttonList.size() / 2))
830 {
831 start_button = (m_buttonList.size() / 2);
832 }
833 else
834 {
835 start_button = m_selPosition;
836 }
837 }
838 else
839 {
840 start_button = 0;
841 }
842
843 break;
844 }
845
846 first_button = last_button = start_button;
847 row_heights.clear();
848
849 // Process row with selected button, and set starting val for m_columns.
850 if (!DistributeRow(first_button, last_button,
851 first_item, last_item, selected_column,
852 skip_cols, grow_left, true, &col_widths,
853 height, 0, 0, col_cnt, wrapped))
854 {
855 delete[] col_widths;
856 return false;
857 }
858
859 m_columns = col_cnt;
860
862 {
863 /*
864 * Now that we know how many columns there are, we can start
865 * the grid layout for real.
866 */
867 start_item = (m_selPosition / m_columns) * m_columns;
868 first_item = last_item = start_item;
869
870 /*
871 * Attempt to pick a start_button which will minimize the need
872 * for new button allocations.
873 */
874#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
875 start_button = std::max(m_buttonList.size() / 2, 0);
876#else
877 start_button = std::max(m_buttonList.size() / 2, static_cast<qsizetype>(0));
878#endif
879 start_button = (start_button / std::max(m_columns, 1)) * m_columns;
880
881 if (start_button < m_itemCount / 2 &&
882 m_itemCount - m_selPosition - 1 < m_buttonList.size() / 2)
883 start_button += m_columns;
884
885 first_button = last_button = start_button;
886
887 // Now do initial row layout again with our new knowledge
888 selected_column = selected_row = -1;
889
890 if (!DistributeRow(first_button, last_button,
891 first_item, last_item, selected_column,
892 skip_cols, grow_left, true, &col_widths,
893 height, 0, 0, col_cnt, wrapped))
894 {
895 delete[] col_widths;
896 return false;
897 }
898 }
899
900 if (selected_column != -1)
901 selected_row = 0;
902
903 row_heights.push_back(height);
904
906 top_height = bottom_height = (height / 2);
907 else
908 bottom_height = height;
909
911 break;
912
913 // As as many columns as will fit.
914 if (DistributeCols(first_button, last_button,
915 first_item, last_item,
916 selected_column, selected_row,
917 skip_cols, &col_widths, row_heights,
918 top_height, bottom_height, wrapped))
919 break; // Buttons fit on each row, so done
920
921 delete[] col_widths;
922 col_widths = nullptr;
923
924 --m_columns;
925 start_item = m_selPosition;
926 }
927
928 m_rows = row_heights.size();
929
930 LOG(VB_GUI, LOG_DEBUG,
931 QString("%1 rows, %2 columns fit inside parent area %3x%4")
932 .arg(m_rows).arg(m_columns).arg(m_contentsRect.width())
933 .arg(m_contentsRect.height()));
934
935 if (col_widths == nullptr)
936 return false;
937
938 int total = 0;
939 int left_spacing = 0;
940 int right_spacing = 0;
941 int top_spacing = 0;
942 int bottom_spacing = 0;
943 MythRect min_rect;
944 QString status_msg;
945
946 /*
947 * Calculate heights of buttons on each side of selected button
948 */
949 top_height = bottom_height = m_topRows = m_bottomRows = 0;
950
951 status_msg = "Row heights: ";
952
953 for (int row = 0; row < m_rows; ++row)
954 {
955 if (row != 0)
956 status_msg += ", ";
957
958 if (row == selected_row)
959 {
960 status_msg += '[';
961 top_height += (row_heights[row] / 2);
962 bottom_height += ((row_heights[row] / 2) + (row_heights[row] % 2));
963 }
964 else
965 {
966 if (bottom_height)
967 {
968 bottom_height += m_itemVertSpacing + row_heights[row];
969 ++m_bottomRows;
970 }
971 else
972 {
973 top_height += row_heights[row] + m_itemVertSpacing;
974 ++m_topRows;
975 }
976 }
977
978 status_msg += QString("%1").arg(row_heights[row]);
979
980 if (row == selected_row)
981 status_msg += ']';
982 }
983
984 /*
985 * How much extra space should there be between buttons?
986 */
988 {
989 // None
990 top_spacing = bottom_spacing = 0;
991 }
992 else
993 {
994 if (m_rows < 2)
995 {
996 // Equal space on both sides of single row
997 top_spacing = bottom_spacing =
998 (m_contentsRect.height() - top_height) / 2;
999 }
1000 else
1001 {
1003 {
1004 // Selected button needs to end up in the middle of area
1005 top_spacing = m_topRows ? ((m_contentsRect.height() / 2) -
1006 top_height) / m_topRows : 0;
1007 bottom_spacing = m_bottomRows ? ((m_contentsRect.height() / 2) -
1008 bottom_height) / m_bottomRows : 0;
1009
1010 if (m_arrange == ArrangeSpread)
1011 {
1012 // Use same spacing on both sides of selected button
1013 if (!m_topRows || top_spacing > bottom_spacing)
1014 top_spacing = bottom_spacing;
1015 else
1016 bottom_spacing = top_spacing;
1017 }
1018 }
1019 else
1020 {
1021 // Buttons will be evenly spread out to fill entire area
1022 top_spacing = bottom_spacing = (m_contentsRect.height() -
1023 (top_height + bottom_height)) /
1025 }
1026 }
1027
1028 // Add in intra-button space size
1029 top_height += (top_spacing * m_topRows);
1030 bottom_height += (bottom_spacing * m_bottomRows);
1031 }
1032
1033 /*
1034 * Calculate top margin
1035 */
1036 int y = m_contentsRect.y();
1037
1038 if ((alignment & Qt::AlignVCenter) && m_arrange != ArrangeFill)
1039 {
1041 {
1042 // Adjust to compensate for top height less than bottom height
1043 y += std::max(bottom_height - top_height, 0);
1044 total = std::max(top_height, bottom_height) * 2;
1045 }
1046 else
1047 {
1048 total = top_height + bottom_height;
1049 }
1050
1051 // Adjust top margin so selected button ends up in the middle
1052 y += (std::max(m_contentsRect.height() - total, 2) / 2);
1053 }
1054 else if ((alignment & Qt::AlignBottom) && m_arrange == ArrangeStack)
1055 {
1056 // Adjust top margin so buttons are bottom justified
1057 y += std::max(m_contentsRect.height() -
1058 (top_height + bottom_height), 0);
1059 }
1060 min_rect.setY(y);
1061
1062 status_msg += QString(" spacing top %1 bottom %2 fixed %3 offset %4")
1063 .arg(top_spacing).arg(bottom_spacing)
1064 .arg(m_itemVertSpacing).arg(y);
1065
1066 LOG(VB_GUI, LOG_DEBUG, status_msg);
1067
1068 /*
1069 * Calculate width of buttons on each side of selected button
1070 */
1071 int left_width = 0;
1072 int right_width = 0;
1074
1075 status_msg = "Col widths: ";
1076
1077 for (int col = 0; col < m_columns; ++col)
1078 {
1079 if (col != 0)
1080 status_msg += ", ";
1081
1082 if (col == selected_column)
1083 {
1084 status_msg += '[';
1085 left_width += (col_widths[col] / 2);
1086 right_width += ((col_widths[col] / 2) + (col_widths[col] % 2));
1087 }
1088 else
1089 {
1090 if (right_width)
1091 {
1092 right_width += m_itemHorizSpacing + col_widths[col];
1094 }
1095 else
1096 {
1097 left_width += col_widths[col] + m_itemHorizSpacing;
1098 ++m_leftColumns;
1099 }
1100 }
1101
1102 status_msg += QString("%1").arg(col_widths[col]);
1103
1104 if (col == selected_column)
1105 status_msg += ']';
1106 }
1107
1108 /*
1109 * How much extra space should there be between buttons?
1110 */
1112 {
1113 // None
1114 left_spacing = right_spacing = 0;
1115 }
1116 else
1117 {
1118 if (m_columns < 2)
1119 {
1120 // Equal space on both sides of single column
1121 left_spacing = right_spacing =
1122 (m_contentsRect.width() - left_width) / 2;
1123 }
1124 else
1125 {
1127 {
1128 // Selected button needs to end up in the middle
1129 left_spacing = m_leftColumns ? ((m_contentsRect.width() / 2) -
1130 left_width) / m_leftColumns : 0;
1131 right_spacing = m_rightColumns ? ((m_contentsRect.width() / 2) -
1132 right_width) / m_rightColumns : 0;
1133
1134 if (m_arrange == ArrangeSpread)
1135 {
1136 // Use same spacing on both sides of selected button
1137 if (!m_leftColumns || left_spacing > right_spacing)
1138 left_spacing = right_spacing;
1139 else
1140 right_spacing = left_spacing;
1141 }
1142 }
1143 else
1144 {
1145 // Buttons will be evenly spread out to fill entire area
1146 left_spacing = right_spacing = (m_contentsRect.width() -
1147 (left_width + right_width)) /
1149 }
1150 }
1151
1152 // Add in intra-button space size
1153 left_width += (left_spacing * m_leftColumns);
1154 right_width += (right_spacing * m_rightColumns);
1155 }
1156
1157 /*
1158 * Calculate left margin
1159 */
1160 int x_init = m_contentsRect.x();
1161
1162 if ((alignment & Qt::AlignHCenter) && m_arrange != ArrangeFill)
1163 {
1165 {
1166 // Compensate for left being smaller than right
1167 x_init += std::max(right_width - left_width, 0);
1168 total = std::max(left_width, right_width) * 2;
1169 }
1170 else
1171 {
1172 total = left_width + right_width;
1173 }
1174
1175 // Adjust left margin so selected button ends up in the middle
1176 x_init += (std::max(m_contentsRect.width() - total, 2) / 2);
1177 }
1178 else if ((alignment & Qt::AlignRight) && m_arrange == ArrangeStack)
1179 {
1180 // Adjust left margin, so buttons are right justified
1181 x_init += std::max(m_contentsRect.width() -
1182 (left_width + right_width), 0);
1183 }
1184 min_rect.setX(x_init);
1185
1186 status_msg += QString(" spacing left %1 right %2 fixed %3 offset %4")
1187 .arg(left_spacing).arg(right_spacing)
1188 .arg(m_itemHorizSpacing).arg(x_init);
1189 LOG(VB_GUI, LOG_DEBUG, status_msg);
1190
1191 top_spacing += m_itemVertSpacing;
1192 bottom_spacing += m_itemVertSpacing;
1193 left_spacing += m_itemHorizSpacing;
1194 right_spacing += m_itemHorizSpacing;
1195
1196 // Calculate position of each button
1197 int buttonIdx = first_button - skip_cols;
1198 int x = 0;
1199 int x_adj = 0;
1200 int y_adj = 0;
1201
1202 int vertical_spacing = top_spacing;
1203
1204 for (int row = 0; row < m_rows; ++row)
1205 {
1206 x = x_init;
1207 int horizontal_spacing = left_spacing;
1208
1209 for (int col = 0; col < m_columns && buttonIdx <= last_button; ++col)
1210 {
1211 if (buttonIdx >= first_button)
1212 {
1213 MythUIStateType *realButton = m_buttonList[buttonIdx];
1214 auto *buttonstate = dynamic_cast<MythUIGroup *>
1215 (realButton->GetCurrentState());
1216 if (!buttonstate)
1217 break; // Not continue
1218
1219 MythRect area = buttonstate->GetArea();
1220
1221 // Center button within width of column
1222 if (alignment & Qt::AlignHCenter)
1223 x_adj = (col_widths[col] - minButtonWidth(area)) / 2;
1224 else if (alignment & Qt::AlignRight)
1225 x_adj = (col_widths[col] - minButtonWidth(area));
1226 else
1227 x_adj = 0;
1229 x_adj -= area.x(); // Negate button's own offset
1230
1231 // Center button within height of row.
1232 if (alignment & Qt::AlignVCenter)
1233 y_adj = (row_heights[row] - minButtonHeight(area)) / 2;
1234 else if (alignment & Qt::AlignBottom)
1235 y_adj = (row_heights[row] - minButtonHeight(area));
1236 else
1237 y_adj = 0;
1238 if (m_layout == LayoutVertical)
1239 y_adj -= area.y(); // Negate button's own offset
1240
1241 // Set position of button
1242 realButton->SetPosition(x + x_adj, y + y_adj);
1243 realButton->SetVisible(true);
1244
1245 if (col == selected_column)
1246 {
1247 horizontal_spacing = right_spacing;
1248 if (row == selected_row)
1249 realButton->MoveToTop();
1250 }
1251 }
1252 x += col_widths[col] + horizontal_spacing;
1253 ++buttonIdx;
1254 }
1255
1256 if (row == selected_row)
1257 vertical_spacing = bottom_spacing;
1258
1259 y += row_heights[row] + vertical_spacing;
1260 }
1261 min_rect.setWidth(x - min_rect.x());
1262 min_rect.setHeight(y - min_rect.y());
1263
1265
1266 // Hide buttons before first active button
1267 for (buttonIdx = 0; buttonIdx < first_button; ++buttonIdx)
1268 m_buttonList[buttonIdx]->SetVisible(false);
1269
1270 // Hide buttons after last active buttons.
1271 for (buttonIdx = m_maxVisible - 1; buttonIdx > last_button; --buttonIdx)
1272 m_buttonList[buttonIdx]->SetVisible(false);
1273
1274 // Set m_topPosition so arrows are displayed correctly.
1276 m_topPosition = static_cast<int>(m_itemsVisible < m_itemCount);
1277 else
1278 m_topPosition = first_item;
1279
1281 if (m_minSize.isValid())
1282 {
1283 // Record the minimal area needed for the button list
1284 SetMinArea(min_rect);
1285 }
1286
1287 delete[] col_widths;
1288 return true;
1289}
1290
1292{
1293 if (m_buttonList.empty())
1294 return;
1295
1296 int drawFromBottom = IsShadowing() && m_shadowDrawFromBottom ?
1298
1299 int button = 0;
1300
1301 switch (m_scrollStyle)
1302 {
1303 case ScrollCenter:
1304 case ScrollGroupCenter:
1305 m_topPosition = std::max(m_selPosition -
1306 (int)((float)m_itemsVisible / 2), 0);
1307 break;
1308 case ScrollFree:
1309 {
1310 int adjust = 0;
1311
1312 if (m_topPosition == -1 || m_keepSelAtBottom)
1313 {
1314 if (m_topPosition == -1)
1315 m_topPosition = 0;
1316
1318 adjust = 1 - m_itemsVisible;
1319 else
1320 adjust = m_columns - m_itemsVisible;
1321
1322 m_keepSelAtBottom = false;
1323 }
1324
1327 {
1329 m_topPosition = m_selPosition + adjust;
1330 else
1331 m_topPosition = (m_selPosition + adjust) /
1333 }
1334
1335 // Adjusted if last item is deleted
1336 if (((m_itemList.count() - m_topPosition) < m_itemsVisible) &&
1338 m_columns == 1)
1340
1341 m_topPosition = std::max(m_topPosition, 0);
1342 break;
1343 }
1344 }
1345
1346 QList<MythUIButtonListItem *>::iterator it = m_itemList.begin() +
1348
1350 {
1351 if (m_selPosition <= m_itemsVisible / 2)
1352 {
1353 button = (m_itemsVisible / 2) - m_selPosition;
1354
1355 if (m_wrapStyle == WrapItems && button > 0 &&
1357 {
1358 it = m_itemList.end() - button;
1359 button = 0;
1360 }
1361 }
1362 else if ((m_itemCount - m_selPosition) < (m_itemsVisible / 2))
1363 {
1364 it = m_itemList.begin() + m_selPosition - (m_itemsVisible / 2);
1365 }
1366 }
1367 else if (drawFromBottom && m_itemCount < m_itemsVisible)
1368 {
1369 button = m_itemsVisible - m_itemCount;
1370 }
1371
1372 for (int i = 0; i < button; ++i)
1373 m_buttonList[i]->SetVisible(false);
1374
1375 bool seenSelected = false;
1376
1377 MythUIStateType *realButton = nullptr;
1378 MythUIButtonListItem *buttonItem = nullptr;
1379
1380 if (it < m_itemList.begin())
1381 it = m_itemList.begin();
1382
1383 int curItem = it < m_itemList.end() ? GetItemPos(*it) : 0;
1384
1385 while (it < m_itemList.end() && button < m_itemsVisible)
1386 {
1387 realButton = m_buttonList[button];
1388 buttonItem = *it;
1389
1390 if (!realButton || !buttonItem)
1391 break;
1392
1393 bool selected = false;
1394
1395 if (!seenSelected && (curItem == m_selPosition))
1396 {
1397 seenSelected = true;
1398 selected = true;
1399 }
1400
1401 m_buttonToItem[button] = buttonItem;
1402 buttonItem->SetToRealButton(realButton, selected);
1403 realButton->SetVisible(true);
1404
1405 if (m_wrapStyle == WrapItems && it == (m_itemList.end() - 1) &&
1407 {
1408 it = m_itemList.begin();
1409 curItem = 0;
1410 }
1411 else
1412 {
1413 ++it;
1414 ++curItem;
1415 }
1416
1417 ++button;
1418 }
1419
1420 for (; button < m_itemsVisible; ++button)
1421 m_buttonList[button]->SetVisible(false);
1422}
1423
1425{
1426 if (m_selPosition < 0)
1427 m_selPosition = (m_wrapStyle > WrapNone) ? m_itemList.size() - 1 : 0;
1428 else if (m_selPosition >= m_itemList.size())
1429 m_selPosition = (m_wrapStyle > WrapNone) ? 0 : m_itemList.size() - 1;
1430}
1431
1433{
1434 if (!m_initialized)
1435 Init();
1436
1437 if (!m_initialized)
1438 return;
1439
1440 if (m_clearing)
1441 return;
1442
1443 m_needsUpdate = false;
1444
1445 // mark the visible buttons as invisible
1446 QMap<int, MythUIButtonListItem*>::const_iterator i = m_buttonToItem.constBegin();
1447 while (i != m_buttonToItem.constEnd())
1448 {
1449 if (i.value())
1450 i.value()->setVisible(false);
1451 ++i;
1452 }
1453
1454 // set topitem, top position
1456 m_buttonToItem.clear();
1457
1458 if (m_arrange == ArrangeFixed)
1460 else
1462
1463 updateLCD();
1464
1465 m_needsUpdate = false;
1466
1467 if (!m_downArrow || !m_upArrow)
1468 return;
1469
1470 if (m_itemCount == 0)
1471 {
1474 }
1475 else
1476 {
1477 if (m_topPosition != 0)
1479 else
1481
1484 else
1486
1489 }
1490}
1491
1493{
1494 if (item)
1495 emit itemVisible(item);
1496}
1497
1499{
1500 bool wasEmpty = m_itemList.isEmpty();
1501
1502 if (listPosition >= 0 && listPosition <= m_itemList.count())
1503 {
1504 m_itemList.insert(listPosition, item);
1505
1506 if (listPosition <= m_selPosition)
1507 ++m_selPosition;
1508
1509 if (listPosition <= m_topPosition)
1510 ++m_topPosition;
1511 }
1512 else
1513 {
1514 m_itemList.append(item);
1515 }
1516
1517 ++m_itemCount;
1518
1519 if (wasEmpty)
1520 {
1522 emit itemSelected(item);
1523 emit DependChanged(false);
1524 }
1525
1526 Update();
1527}
1528
1530{
1531 if (m_clearing)
1532 return;
1533
1534 int curIndex = m_itemList.indexOf(item);
1535
1536 if (curIndex == -1)
1537 return;
1538
1539 QMap<int, MythUIButtonListItem*>::iterator it = m_buttonToItem.begin();
1540 while (it != m_buttonToItem.end())
1541 {
1542 if (it.value() == item)
1543 {
1544 m_buttonToItem.erase(it);
1545 break;
1546 }
1547 ++it;
1548 }
1549
1550 if (curIndex < m_topPosition &&
1551 m_topPosition > 0)
1552 {
1553 // The removed item is before the visible part, move
1554 // everything up 1. The visible part shouldn't appear to
1555 // change.
1556 --m_topPosition;
1557 --m_selPosition;
1558 }
1559 else if (curIndex < m_selPosition ||
1560 (m_selPosition == m_itemCount - 1 &&
1561 m_selPosition > 0))
1562 {
1563 // The removed item is visible and before the selected item or
1564 // the selected item is the last item but not the only item,
1565 // move the selected item up 1.
1566 --m_selPosition;
1567 }
1568
1569 m_itemList.removeAt(curIndex);
1570 --m_itemCount;
1571
1572 Update();
1573
1576 else
1577 emit itemSelected(nullptr);
1578
1579 if (IsEmpty())
1580 emit DependChanged(true);
1581}
1582
1583void MythUIButtonList::SetValueByData(const QVariant& data)
1584{
1585 if (!m_initialized)
1586 Init();
1587
1588 for (auto *item : std::as_const(m_itemList))
1589 {
1590 if (item->GetData() == data)
1591 {
1592 SetItemCurrent(item);
1593 return;
1594 }
1595 }
1596}
1597
1599{
1600 int newIndex = m_itemList.indexOf(item);
1601 SetItemCurrent(newIndex);
1602}
1603
1605{
1606 if (!m_initialized)
1607 Init();
1608
1609 if (current == -1 || current >= m_itemList.size())
1610 return;
1611
1612 if (!m_itemList.at(current)->isEnabled())
1613 return;
1614
1615 if (current == m_selPosition &&
1616 (topPosition == -1 || topPosition == m_topPosition))
1617 return;
1618
1619 m_topPosition = topPosition;
1620
1621 if (topPosition > 0 && m_layout == LayoutGrid)
1622 m_topPosition -= (topPosition % m_columns);
1623
1625
1626 Update();
1627
1629}
1630
1632{
1633 if (m_itemList.isEmpty() || m_selPosition >= m_itemList.size() ||
1634 m_selPosition < 0)
1635 return nullptr;
1636
1637 return m_itemList.at(m_selPosition);
1638}
1639
1641{
1643
1644 if (item)
1645 return item->GetText().toInt();
1646
1647 return 0;
1648}
1649
1651{
1653
1654 if (item)
1655 return item->GetText();
1656
1657 return {};
1658}
1659
1661{
1663
1664 if (item)
1665 return item->GetData();
1666
1667 return {};
1668}
1669
1671{
1672 if (m_contentsRect.isValid())
1673 return m_contentsRect;
1674 return m_area;
1675}
1676
1678{
1679 if (!m_itemList.empty())
1680 return m_itemList[0];
1681
1682 return nullptr;
1683}
1684
1686const
1687{
1688 // Find item
1689 auto it = std::ranges::find(m_itemList, item);
1690 if (it == m_itemList.end())
1691 return nullptr;
1692 // Return next
1693 return (++it != m_itemList.end()) ? *it : nullptr;
1694}
1695
1697{
1698 return m_itemCount;
1699}
1700
1702{
1703 if (m_needsUpdate)
1704 {
1707 }
1708
1709 return m_itemsVisible;
1710}
1711
1713{
1714 return m_itemCount <= 0;
1715}
1716
1718{
1719 if (pos < 0 || pos >= m_itemList.size())
1720 return nullptr;
1721
1722 return m_itemList.at(pos);
1723}
1724
1726{
1727 if (!m_initialized)
1728 Init();
1729
1730 for (auto *item : std::as_const(m_itemList))
1731 {
1732 if (item->GetData() == data)
1733 return item;
1734 }
1735
1736 return nullptr;
1737}
1738
1740{
1741 if (!item)
1742 return -1;
1743
1744 return m_itemList.indexOf(item);
1745}
1746
1747void MythUIButtonList::InitButton(int itemIdx, MythUIStateType* & realButton,
1748 MythUIButtonListItem* & buttonItem)
1749{
1750 buttonItem = m_itemList[itemIdx];
1751
1752 if (m_maxVisible == 0)
1753 {
1754 QString name("buttonlist button 0");
1755 auto *button = new MythUIStateType(this, name);
1756 button->CopyFrom(m_buttontemplate);
1757 button->ConnectDependants(true);
1758 m_buttonList.append(button);
1759 ++m_maxVisible;
1760 }
1761
1762 realButton = m_buttonList[0];
1763 m_buttonToItem[0] = buttonItem;
1764}
1765
1766/*
1767 * PageUp and PageDown are helpers when Dynamic layout is being used.
1768 *
1769 * When buttons are layed out dynamically, the number of buttons on the next
1770 * page, may not equal the number of buttons on the current page. Dynamic
1771 * layout is always center-weighted, so attempt to figure out which button
1772 * is near the middle on the next page.
1773 */
1774
1776{
1777 int pos = m_selPosition;
1778 int total = 0;
1779
1780 /*
1781 * /On the new page/
1782 * If the number of buttons before the selected button does not equal
1783 * the number of buttons after the selected button, this logic can
1784 * undershoot the new selected button. That is better than overshooting
1785 * though.
1786 *
1787 * To fix this would require laying out the new page and then figuring
1788 * out which button should be selected, but this is already complex enough.
1789 */
1790
1792 {
1793 pos -= (m_leftColumns + 1);
1794
1795 int max_width = m_contentsRect.width() / 2;
1796
1797 for (; pos >= 0; --pos)
1798 {
1799 MythUIStateType *realButton = nullptr;
1800 MythUIButtonListItem *buttonItem = nullptr;
1801 InitButton(pos, realButton, buttonItem);
1802 buttonItem->SetToRealButton(realButton, true);
1803 auto *buttonstate = dynamic_cast<MythUIGroup *>
1804 (realButton->GetCurrentState());
1805
1806 if (buttonstate == nullptr)
1807 {
1808 LOG(VB_GENERAL, LOG_ERR,
1809 "PageUp: Failed to query buttonlist state");
1810 return pos;
1811 }
1812
1813 if (total + m_itemHorizSpacing +
1814 (buttonstate->GetArea().width() / 2) >= max_width)
1815 return pos + 1;
1816
1817 buttonItem->SetToRealButton(realButton, false);
1818 buttonstate = dynamic_cast<MythUIGroup *>
1819 (realButton->GetCurrentState());
1820 if (buttonstate)
1821 total += m_itemHorizSpacing + buttonstate->GetArea().width();
1822 }
1823
1824 return 0;
1825 }
1826
1827 // Grid or Vertical
1828 int dec = 1;
1829
1830 if (m_layout == LayoutGrid)
1831 {
1832 /*
1833 * Adjusting using bottomRow:TopRow only works if new page
1834 * has the same ratio as the previous page, but that is common
1835 * with the grid layout, so go for it. If themers start doing
1836 * grids where this is not true, then this will need to be modified.
1837 */
1838 pos -= (m_columns * (m_topRows + 2 +
1839 std::max(m_bottomRows - m_topRows, 0)));
1840 dec = m_columns;
1841 }
1842 else
1843 {
1844 pos -= (m_topRows + 1);
1845 dec = 1;
1846 }
1847
1848 int max_height = m_contentsRect.height() / 2;
1849
1850 for (; pos >= 0; pos -= dec)
1851 {
1852 MythUIStateType *realButton = nullptr;
1853 MythUIButtonListItem *buttonItem = nullptr;
1854 InitButton(pos, realButton, buttonItem);
1855 buttonItem->SetToRealButton(realButton, true);
1856 auto *buttonstate = dynamic_cast<MythUIGroup *>
1857 (realButton->GetCurrentState());
1858
1859 if (buttonstate == nullptr)
1860 {
1861 LOG(VB_GENERAL, LOG_ERR,
1862 "PageUp: Failed to query buttonlist state");
1863 return pos;
1864 }
1865
1866 if (total + m_itemHorizSpacing +
1867 (buttonstate->GetArea().height() / 2) >= max_height)
1868 return pos + dec;
1869
1870 buttonItem->SetToRealButton(realButton, false);
1871 buttonstate = dynamic_cast<MythUIGroup *>
1872 (realButton->GetCurrentState());
1873 if (buttonstate)
1874 total += m_itemHorizSpacing + buttonstate->GetArea().height();
1875 }
1876
1877 return 0;
1878}
1879
1881{
1882 int pos = m_selPosition;
1883 int num_items = m_itemList.size();
1884 int total = 0;
1885
1886 /*
1887 * /On the new page/
1888 * If the number of buttons before the selected button does not equal
1889 * the number of buttons after the selected button, this logic can
1890 * undershoot the new selected button. That is better than overshooting
1891 * though.
1892 *
1893 * To fix this would require laying out the new page and then figuring
1894 * out which button should be selected, but this is already complex enough.
1895 */
1896
1898 {
1899 pos += (m_rightColumns + 1);
1900
1901 int max_width = m_contentsRect.width() / 2;
1902
1903 for (; pos < num_items; ++pos)
1904 {
1905 MythUIStateType *realButton = nullptr;
1906 MythUIButtonListItem *buttonItem = nullptr;
1907 InitButton(pos, realButton, buttonItem);
1908 buttonItem->SetToRealButton(realButton, true);
1909 auto *buttonstate = dynamic_cast<MythUIGroup *>
1910 (realButton->GetCurrentState());
1911
1912 if (buttonstate == nullptr)
1913 {
1914 LOG(VB_GENERAL, LOG_ERR,
1915 "PageDown: Failed to query buttonlist state");
1916 return pos;
1917 }
1918
1919 if (total + m_itemHorizSpacing +
1920 (buttonstate->GetArea().width() / 2) >= max_width)
1921 return pos - 1;
1922
1923 buttonItem->SetToRealButton(realButton, false);
1924 buttonstate = dynamic_cast<MythUIGroup *>
1925 (realButton->GetCurrentState());
1926 if (buttonstate)
1927 total += m_itemHorizSpacing + buttonstate->GetArea().width();
1928 }
1929
1930 return num_items - 1;
1931 }
1932
1933 // Grid or Vertical
1934 int inc = 1;
1935
1936 if (m_layout == LayoutGrid)
1937 {
1938 /*
1939 * Adjusting using bottomRow:TopRow only works if new page
1940 * has the same ratio as the previous page, but that is common
1941 * with the grid layout, so go for it. If themers start doing
1942 * grids where this is not true, then this will need to be modified.
1943 */
1944 pos += (m_columns * (m_bottomRows + 2 +
1945 std::max(m_topRows - m_bottomRows, 0)));
1946 inc = m_columns;
1947 }
1948 else
1949 {
1950 pos += (m_bottomRows + 1);
1951 inc = 1;
1952 }
1953
1954 int max_height = m_contentsRect.height() / 2;
1955
1956 for (; pos < num_items; pos += inc)
1957 {
1958 MythUIStateType *realButton = nullptr;
1959 MythUIButtonListItem *buttonItem = nullptr;
1960 InitButton(pos, realButton, buttonItem);
1961 buttonItem->SetToRealButton(realButton, true);
1962 auto *buttonstate = dynamic_cast<MythUIGroup *>
1963 (realButton->GetCurrentState());
1964
1965 if (!buttonstate)
1966 {
1967 LOG(VB_GENERAL, LOG_ERR,
1968 "PageDown: Failed to query buttonlist state");
1969 return pos;
1970 }
1971
1972 if (total + m_itemHorizSpacing +
1973 (buttonstate->GetArea().height() / 2) >= max_height)
1974 return pos - inc;
1975
1976 buttonItem->SetToRealButton(realButton, false);
1977 buttonstate = dynamic_cast<MythUIGroup *>
1978 (realButton->GetCurrentState());
1979 if (buttonstate)
1980 total += m_itemHorizSpacing + buttonstate->GetArea().height();
1981 }
1982
1983 return num_items - 1;
1984}
1985
1987{
1988 int pos = m_selPosition;
1989
1990 if (pos == -1 || m_itemList.isEmpty() || !m_initialized)
1991 return false;
1992
1993 switch (unit)
1994 {
1995 case MoveItem:
1996 if (m_selPosition > 0)
1997 --m_selPosition;
1998 else if (m_wrapStyle > WrapNone)
1999 m_selPosition = m_itemList.size() - 1;
2000 else if (m_wrapStyle == WrapCaptive)
2001 return true;
2002
2003 FindEnabledUp(unit);
2004
2005 break;
2006
2007 case MoveColumn:
2008 if (pos % m_columns > 0)
2009 {
2010 --m_selPosition;
2011 }
2012 else if (m_wrapStyle == WrapFlowing)
2013 {
2014 if (m_selPosition == 0)
2015 --m_selPosition = m_itemList.size() - 1;
2016 else
2017 --m_selPosition;
2018 }
2019 else if (m_wrapStyle > WrapNone)
2020 {
2021 m_selPosition = pos + (m_columns - 1);
2022 }
2023 else if (m_wrapStyle == WrapCaptive)
2024 {
2025 return true;
2026 }
2027
2028 FindEnabledUp(unit);
2029
2030 break;
2031
2032 case MoveRow:
2034 {
2036 if (m_selPosition < 0)
2037 m_selPosition += m_itemList.size();
2038 else
2039 m_selPosition %= m_itemList.size();
2040 }
2041 else if ((pos - m_columns) >= 0)
2042 {
2044 }
2045 else if (m_wrapStyle > WrapNone)
2046 {
2047 m_selPosition = (((m_itemList.size() - 1) / m_columns) *
2048 m_columns) + pos;
2049
2050 if ((m_selPosition / m_columns)
2051 < ((m_itemList.size() - 1) / m_columns))
2052 m_selPosition = m_itemList.size() - 1;
2053
2054 if (m_layout == LayoutVertical)
2055 m_topPosition = std::max(0, m_selPosition - m_itemsVisible + 1);
2056 }
2057 else if (m_wrapStyle == WrapCaptive)
2058 {
2059 return true;
2060 }
2061
2062 FindEnabledUp(unit);
2063
2064 break;
2065
2066 case MovePage:
2067 if (m_arrange == ArrangeFixed)
2069 else
2071
2072 FindEnabledUp(unit);
2073
2074 break;
2075
2076 case MoveMid:
2077 m_selPosition = m_itemList.size() / 2;
2078 FindEnabledUp(unit);
2079 break;
2080
2081 case MoveMax:
2082 m_selPosition = 0;
2083 FindEnabledUp(unit);
2084 break;
2085
2086 case MoveByAmount:
2087 for (uint i = 0; i < amount; ++i)
2088 {
2089 if (m_selPosition > 0)
2090 --m_selPosition;
2091 else if (m_wrapStyle > WrapNone)
2092 m_selPosition = m_itemList.size() - 1;
2093 }
2094
2095 FindEnabledUp(unit);
2096
2097 break;
2098 }
2099
2101
2102 if (pos != m_selPosition)
2103 {
2104 Update();
2106 }
2107 else
2108 {
2109 return false;
2110 }
2111
2112 return true;
2113}
2114
2115
2120{
2121 if (m_selPosition < 0 || m_selPosition >= m_itemList.size() ||
2122 m_itemList.at(m_selPosition)->isEnabled())
2123 return;
2124
2125 int step = (unit == MoveRow) ? m_columns : 1;
2126 if (unit == MoveRow && m_wrapStyle == WrapFlowing)
2127 unit = MoveItem;
2128 if (unit == MoveColumn)
2129 {
2130 while (m_selPosition < m_itemList.size() &&
2131 (m_selPosition + 1) % m_columns > 0 &&
2132 !m_itemList.at(m_selPosition)->isEnabled())
2133 ++m_selPosition;
2134
2135 if (m_itemList.at(m_selPosition)->isEnabled())
2136 return;
2137
2138 if (m_wrapStyle > WrapNone)
2139 {
2141 while ((m_selPosition + 1) % m_columns > 0 &&
2142 !m_itemList.at(m_selPosition)->isEnabled())
2143 ++m_selPosition;
2144 }
2145 }
2146 else
2147 {
2148 while (!m_itemList.at(m_selPosition)->isEnabled() &&
2149 (m_selPosition < m_itemList.size() - step))
2150 m_selPosition += step;
2151
2152 if (!m_itemList.at(m_selPosition)->isEnabled() &&
2154 {
2155 m_selPosition = (m_selPosition + step) % m_itemList.size();
2156
2157 while (!m_itemList.at(m_selPosition)->isEnabled() &&
2158 (m_selPosition < m_itemList.size() - step))
2159 m_selPosition += step;
2160 }
2161 }
2162}
2163
2165{
2166 if (m_selPosition < 0 || m_selPosition >= m_itemList.size() ||
2167 m_itemList.at(m_selPosition)->isEnabled())
2168 return;
2169
2170 int step = (unit == MoveRow) ? m_columns : 1;
2171 if (unit == MoveRow && m_wrapStyle == WrapFlowing)
2172 unit = MoveItem;
2173 if (unit == MoveColumn)
2174 {
2175 while (m_selPosition > 0 && (m_selPosition - 1) % m_columns > 0 &&
2176 !m_itemList.at(m_selPosition)->isEnabled())
2177 --m_selPosition;
2178
2179 if (m_itemList.at(m_selPosition)->isEnabled())
2180 return;
2181
2182 if (m_wrapStyle > WrapNone)
2183 {
2185 while ((m_selPosition - 1) % m_columns > 0 &&
2186 !m_itemList.at(m_selPosition)->isEnabled())
2187 --m_selPosition;
2188 }
2189 }
2190 else
2191 {
2192 while (!m_itemList.at(m_selPosition)->isEnabled() &&
2193 (m_selPosition - step >= 0))
2194 m_selPosition -= step;
2195
2196 if (!m_itemList.at(m_selPosition)->isEnabled() &&
2198 {
2199 m_selPosition = m_itemList.size() - 1;
2200
2201 while (m_selPosition > 0 &&
2202 !m_itemList.at(m_selPosition)->isEnabled() &&
2203 (m_selPosition - step >= 0))
2204 m_selPosition -= step;
2205 }
2206 }
2207}
2208
2209
2211{
2212 int pos = m_selPosition;
2213
2214 if (pos == -1 || m_itemList.isEmpty() || !m_initialized)
2215 return false;
2216
2217 switch (unit)
2218 {
2219 case MoveItem:
2220 if (m_selPosition < m_itemList.size() - 1)
2221 ++m_selPosition;
2222 else if (m_wrapStyle > WrapNone)
2223 m_selPosition = 0;
2224 else if (m_wrapStyle == WrapCaptive)
2225 return true;
2226
2227 FindEnabledDown(unit);
2228
2229 break;
2230
2231 case MoveColumn:
2232 if ((pos + 1) % m_columns > 0)
2233 {
2234 ++m_selPosition;
2235 }
2236 else if (m_wrapStyle == WrapFlowing)
2237 {
2238 if (m_selPosition < m_itemList.size() - 1)
2239 ++m_selPosition;
2240 else
2241 m_selPosition = 0;
2242 }
2243 else if (m_wrapStyle > WrapNone)
2244 {
2245 m_selPosition = pos - (m_columns - 1);
2246 }
2247 else if (m_wrapStyle == WrapCaptive)
2248 {
2249 return true;
2250 }
2251
2252 FindEnabledDown(unit);
2253
2254 break;
2255
2256 case MoveRow:
2257 if (m_itemList.empty() || m_columns < 1)
2258 return true;
2260 {
2262 m_selPosition %= m_itemList.size();
2263 }
2264 else if (((m_itemList.size() - 1) / std::max(m_columns, 0))
2265 > (pos / m_columns))
2266 {
2268 if (m_selPosition >= m_itemList.size())
2269 m_selPosition = m_itemList.size() - 1;
2270 }
2271 else if (m_wrapStyle > WrapNone)
2272 {
2273 m_selPosition = (pos % m_columns);
2274 }
2275 else if (m_wrapStyle == WrapCaptive)
2276 {
2277 return true;
2278 }
2279
2280 FindEnabledDown(unit);
2281
2282 break;
2283
2284 case MovePage:
2285 if (m_arrange == ArrangeFixed)
2286 {
2287 m_selPosition = std::min(m_itemCount - 1,
2289 }
2290 else
2291 {
2293 }
2294
2295 FindEnabledDown(unit);
2296
2297 break;
2298
2299 case MoveMax:
2301 FindEnabledDown(unit);
2302 break;
2303
2304 case MoveByAmount:
2305 for (uint i = 0; i < amount; ++i)
2306 {
2307 if (m_selPosition < m_itemList.size() - 1)
2308 ++m_selPosition;
2309 else if (m_wrapStyle > WrapNone)
2310 m_selPosition = 0;
2311 }
2312 FindEnabledDown(unit);
2313 break;
2314
2315 case MoveMid:
2316 break;
2317 }
2318
2320
2321 if (pos != m_selPosition)
2322 {
2323 m_keepSelAtBottom = true;
2324 Update();
2326 }
2327 else
2328 {
2329 return false;
2330 }
2331
2332 return true;
2333}
2334
2335bool MythUIButtonList::MoveToNamedPosition(const QString &position_name)
2336{
2337 if (!m_initialized)
2338 Init();
2339
2340 if (m_selPosition < 0 || m_itemList.isEmpty() || !m_initialized)
2341 return false;
2342
2343 bool found_it = false;
2344 int selectedPosition = 0;
2345 QList<MythUIButtonListItem *>::iterator it = m_itemList.begin();
2346
2347 while (it != m_itemList.end())
2348 {
2349 if ((*it)->GetText() == position_name)
2350 {
2351 found_it = true;
2352 break;
2353 }
2354
2355 ++it;
2356 ++selectedPosition;
2357 }
2358
2359 if (!found_it || m_selPosition == selectedPosition)
2360 return false;
2361
2362 SetItemCurrent(selectedPosition);
2363 return true;
2364}
2365
2367{
2368 if (GetItemCurrent() != item)
2369 return false;
2370
2371 if (item == m_itemList.first() && up)
2372 return false;
2373
2374 if (item == m_itemList.last() && !up)
2375 return false;
2376
2377 int oldpos = m_selPosition;
2378 int insertat = 0;
2379 bool dolast = false;
2380
2381 if (up)
2382 {
2383 insertat = m_selPosition - 1;
2384
2385 if (item == m_itemList.last())
2386 dolast = true;
2387 else
2388 ++m_selPosition;
2389
2390 if (item == m_itemList.at(m_topPosition))
2391 ++m_topPosition;
2392 }
2393 else
2394 {
2395 insertat = m_selPosition + 1;
2396 }
2397
2398 m_itemList.removeAt(oldpos);
2399 m_itemList.insert(insertat, item);
2400
2401 if (up)
2402 {
2403 MoveUp();
2404
2405 if (!dolast)
2406 MoveUp();
2407 }
2408 else
2409 {
2410 MoveDown();
2411 }
2412
2413 return true;
2414}
2415
2417{
2418 for (const auto & it : std::as_const(m_itemList)) {
2419 it->setChecked(state);
2420 }
2421}
2422
2424{
2425 if (m_initialized)
2426 return;
2427
2428 m_upArrow = dynamic_cast<MythUIStateType *>(GetChild("upscrollarrow"));
2429 m_downArrow = dynamic_cast<MythUIStateType *>(GetChild("downscrollarrow"));
2430 m_scrollBar = dynamic_cast<MythUIScrollBar *>(GetChild("scrollbar"));
2431
2432 if (m_upArrow)
2433 m_upArrow->SetVisible(true);
2434
2435 if (m_downArrow)
2436 m_downArrow->SetVisible(true);
2437
2438 if (m_scrollBar)
2440
2442
2443 m_buttontemplate = dynamic_cast<MythUIStateType *>(GetChild("buttonitem"));
2444
2445 if (!m_buttontemplate)
2446 {
2447 LOG(VB_GENERAL, LOG_ERR, QString("(%1) Statetype buttonitem is "
2448 "required in mythuibuttonlist: %2")
2449 .arg(GetXMLLocation(), objectName()));
2450 return;
2451 }
2452
2454
2455 MythRect buttonItemArea;
2456
2457 MythUIGroup *buttonActiveState = dynamic_cast<MythUIGroup *>
2458 (m_buttontemplate->GetState("active"));
2459
2460 if (buttonActiveState)
2461 buttonItemArea = buttonActiveState->GetArea();
2462 else
2463 buttonItemArea = m_buttontemplate->GetArea();
2464
2465 buttonItemArea.CalculateArea(m_contentsRect);
2466
2467 m_itemHeight = buttonItemArea.height();
2468 m_itemWidth = buttonItemArea.width();
2469
2470 /*
2471 * If fixed spacing is defined, then use the "active" state size
2472 * to predictively determine the position of each button.
2473 */
2474 if (m_arrange == ArrangeFixed)
2475 {
2476
2478
2479 int col = 1;
2480 int row = 1;
2481
2482 for (int i = 0; i < m_itemsVisible; ++i)
2483 {
2484 QString name = QString("buttonlist button %1").arg(i);
2485 auto *button = new MythUIStateType(this, name);
2486 button->CopyFrom(m_buttontemplate);
2487 button->ConnectDependants(true);
2488
2489 if (col > m_columns)
2490 {
2491 col = 1;
2492 ++row;
2493 }
2494
2495 button->SetPosition(GetButtonPosition(col, row));
2496 ++col;
2497
2498 m_buttonList.push_back(button);
2499 }
2500 }
2501
2502 // The following is pretty much a hack for the benefit of MythGallery
2503 // it scales images based on the button size and we need to give it the
2504 // largest button state so that the images are not too small
2505 // This can be removed once the disk based image caching is added to
2506 // mythui, since the mythgallery thumbnail generator can be ditched.
2507 MythUIGroup *buttonSelectedState = dynamic_cast<MythUIGroup *>
2508 (m_buttontemplate->GetState("selected"));
2509
2510 if (buttonSelectedState)
2511 {
2512 MythRect itemArea = buttonSelectedState->GetArea();
2513 itemArea.CalculateArea(m_contentsRect);
2514
2515 m_itemHeight = std::max(m_itemHeight, itemArea.height());
2516
2517 m_itemWidth = std::max(m_itemWidth, itemArea.width());
2518 }
2519
2520 // End Hack
2521
2522 m_initialized = true;
2523}
2524
2526{
2527 if (!m_initialized)
2528 Init();
2529
2530 return static_cast<uint>(m_itemWidth);
2531}
2532
2534{
2535 if (!m_initialized)
2536 Init();
2537
2538 return static_cast<uint>(m_itemHeight);
2539}
2540
2545{
2546 QStringList actions;
2547 bool handled = false;
2548 handled = GetMythMainWindow()->TranslateKeyPress("Global", event, actions);
2549
2550 // Handle action remappings
2551 for (const QString& action : std::as_const(actions))
2552 {
2553 if (!m_actionRemap.contains(action))
2554 continue;
2555
2556 QString key = m_actionRemap[action];
2557 if (key.isEmpty())
2558 return true;
2559
2560 QKeySequence a(key);
2561 if (a.isEmpty())
2562 continue;
2563
2564#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
2565 int keyCode = a[0];
2566 Qt::KeyboardModifiers modifiers = Qt::NoModifier;
2567 QStringList parts = key.split('+');
2568 for (int j = 0; j < parts.count(); ++j)
2569 {
2570 if (parts[j].toUpper() == "CTRL")
2571 modifiers |= Qt::ControlModifier;
2572 if (parts[j].toUpper() == "SHIFT")
2573 modifiers |= Qt::ShiftModifier;
2574 if (parts[j].toUpper() == "ALT")
2575 modifiers |= Qt::AltModifier;
2576 if (parts[j].toUpper() == "META")
2577 modifiers |= Qt::MetaModifier;
2578 }
2579#else
2580 int keyCode = a[0].key();
2581 Qt::KeyboardModifiers modifiers = a[0].keyboardModifiers();
2582#endif
2583
2584 QCoreApplication::postEvent(
2586 new QKeyEvent(QEvent::KeyPress, keyCode, modifiers, key));
2587 QCoreApplication::postEvent(
2589 new QKeyEvent(QEvent::KeyRelease, keyCode, modifiers, key));
2590
2591 return true;
2592 }
2593
2594 // handle actions for this container
2595 for (int i = 0; i < actions.size() && !handled; ++i)
2596 {
2597 const QString& action = actions[i];
2598 handled = true;
2599
2600 if (action == "UP")
2601 {
2602 if ((m_layout == LayoutVertical) || (m_layout == LayoutGrid))
2603 handled = MoveUp(MoveRow);
2604 else
2605 handled = false;
2606 }
2607 else if (action == "DOWN")
2608 {
2609 if ((m_layout == LayoutVertical) || (m_layout == LayoutGrid))
2610 handled = MoveDown(MoveRow);
2611 else
2612 handled = false;
2613 }
2614 else if (action == "RIGHT")
2615 {
2617 {
2618 handled = MoveDown(MoveItem);
2619 }
2620 else if (m_layout == LayoutGrid)
2621 {
2623 handled = MoveDown(MoveColumn);
2624 else
2625 handled = MoveDown(MoveItem);
2626 }
2627 else
2628 {
2629 handled = false;
2630 }
2631 }
2632 else if (action == "LEFT")
2633 {
2635 {
2636 handled = MoveUp(MoveItem);
2637 }
2638 else if (m_layout == LayoutGrid)
2639 {
2641 handled = MoveUp(MoveColumn);
2642 else
2643 handled = MoveUp(MoveItem);
2644 }
2645 else
2646 {
2647 handled = false;
2648 }
2649 }
2650 else if (action == "PAGEUP")
2651 {
2653 }
2654 else if (action == "PAGEDOWN")
2655 {
2657 }
2658 else if (action == "PAGETOP")
2659 {
2660 MoveUp(MoveMax);
2661 }
2662 else if (action == "PAGEMIDDLE")
2663 {
2664 MoveUp(MoveMid);
2665 }
2666 else if (action == "PAGEBOTTOM")
2667 {
2669 }
2670 else if (action == "SELECT")
2671 {
2673
2674 if (item && item->isEnabled())
2675 emit itemClicked(item);
2676 }
2677 else if (action == "SEARCH")
2678 {
2680 }
2681 else
2682 {
2683 handled = false;
2684 }
2685 }
2686
2687 return handled;
2688}
2689
2694{
2695 bool handled = false;
2696
2697 switch (event->GetGesture())
2698 {
2700 {
2701 // We want the relative position of the click
2702 QPoint position = event->GetPosition() -
2704
2705 MythUIType *type = GetChildAt(position, false, false);
2706
2707 if (!type)
2708 return false;
2709
2710 auto *object = dynamic_cast<MythUIStateType *>(type);
2711 if (object)
2712 {
2713 handled = true;
2714 QString name = object->objectName();
2715
2716 if (name == "upscrollarrow")
2717 {
2719 }
2720 else if (name == "downscrollarrow")
2721 {
2723 }
2724 else if (name.startsWith("buttonlist button"))
2725 {
2726 int pos = name.section(' ', 2, 2).toInt();
2727 MythUIButtonListItem *item = m_buttonToItem.value(pos);
2728
2729 if (item)
2730 {
2731 if (item == GetItemCurrent())
2732 emit itemClicked(item);
2733 else
2734 SetItemCurrent(item);
2735 }
2736 }
2737 else
2738 {
2739 handled = false;
2740 }
2741 }
2742 }
2743 break;
2744
2748 if ((m_layout == LayoutVertical) || (m_layout == LayoutGrid))
2749 handled = MoveUp(MoveRow);
2750 break;
2751
2755 if ((m_layout == LayoutVertical) || (m_layout == LayoutGrid))
2756 handled = MoveDown(MoveRow);
2757 break;
2758
2761 {
2762 handled = MoveDown(MoveItem);
2763 }
2764 else if (m_layout == LayoutGrid)
2765 {
2767 handled = MoveDown(MoveColumn);
2768 else
2769 handled = MoveDown(MoveItem);
2770 }
2771 break;
2772
2775 {
2776 handled = MoveUp(MoveItem);
2777 }
2778 else if (m_layout == LayoutGrid)
2779 {
2781 handled = MoveUp(MoveColumn);
2782 else
2783 handled = MoveUp(MoveItem);
2784 }
2785 break;
2786
2787 default:
2788 break;
2789 }
2790
2791 return handled;
2792}
2793
2794class NextButtonListPageEvent : public QEvent
2795{
2796 public:
2797 NextButtonListPageEvent(int start, int pageSize) :
2798 QEvent(kEventType), m_start(start), m_pageSize(pageSize) {}
2799 const int m_start;
2800 const int m_pageSize;
2801 static const Type kEventType;
2802};
2803
2804const QEvent::Type NextButtonListPageEvent::kEventType =
2805 (QEvent::Type) QEvent::registerEventType();
2806
2808{
2809 if (event->type() == NextButtonListPageEvent::kEventType)
2810 {
2811 if (auto *npe = dynamic_cast<NextButtonListPageEvent*>(event); npe)
2812 {
2813 int cur = npe->m_start;
2814 for (; cur < npe->m_start + npe->m_pageSize && cur < GetCount(); ++cur)
2815 {
2816 const int loginterval = (cur < 1000 ? 100 : 500);
2817 if (cur > 200 && cur % loginterval == 0)
2818 LOG(VB_GUI, LOG_INFO,
2819 QString("Build background buttonlist item %1").arg(cur));
2820 emit itemLoaded(GetItemAt(cur));
2821 }
2822 m_nextItemLoaded = cur;
2823 if (cur < GetCount())
2824 LoadInBackground(cur, npe->m_pageSize);
2825 }
2826 }
2827}
2828
2829void MythUIButtonList::LoadInBackground(int start, int pageSize)
2830{
2831 m_nextItemLoaded = start;
2832 QCoreApplication::
2833 postEvent(this, new NextButtonListPageEvent(start, pageSize));
2834}
2835
2837{
2838 QCoreApplication::
2839 removePostedEvents(this, NextButtonListPageEvent::kEventType);
2840 return m_nextItemLoaded;
2841}
2842
2843QPoint MythUIButtonList::GetButtonPosition(int column, int row) const
2844{
2845 int x = m_contentsRect.x() +
2846 ((column - 1) * (m_itemWidth + m_itemHorizSpacing));
2847 int y = m_contentsRect.y() +
2848 ((row - 1) * (m_itemHeight + m_itemVertSpacing));
2849
2850 return {x, y};
2851}
2852
2854{
2855 m_itemsVisible = 0;
2856 m_rows = 0;
2857 m_columns = 0;
2858
2860 {
2861 int x = 0;
2862
2863 while (x <= m_contentsRect.width() - m_itemWidth)
2864 {
2866 ++m_columns;
2867 }
2868 }
2869
2870 if ((m_layout == LayoutVertical) || (m_layout == LayoutGrid))
2871 {
2872 int y = 0;
2873
2874 while (y <= m_contentsRect.height() - m_itemHeight)
2875 {
2877 ++m_rows;
2878 }
2879 }
2880
2881 if (m_rows <= 0)
2882 m_rows = 1;
2883
2884 if (m_columns <= 0)
2885 m_columns = 1;
2886
2888}
2889
2891{
2892 if (rect == m_contentsRect)
2893 return;
2894
2895 m_contentsRect = rect;
2896
2897 if (m_area.isValid())
2899 else if (m_parent)
2901 else
2902 m_contentsRect.CalculateArea(GetMythMainWindow()->GetUIScreenRect());
2903}
2904
2909 const QString &filename, QDomElement &element, bool showWarnings)
2910{
2911 if (element.tagName() == "buttonarea")
2912 {
2913 SetButtonArea(parseRect(element));
2914 }
2915 else if (element.tagName() == "layout")
2916 {
2917 QString layout = getFirstText(element).toLower();
2918
2919 if (layout == "grid")
2921 else if (layout == "horizontal")
2923 else
2925 }
2926 else if (element.tagName() == "arrange")
2927 {
2928 QString arrange = getFirstText(element).toLower();
2929
2930 if (arrange == "fill")
2932 else if (arrange == "spread")
2934 else if (arrange == "stack")
2936 else
2938
2939 }
2940 else if (element.tagName() == "align")
2941 {
2942 QString align = getFirstText(element).toLower();
2944 }
2945 else if (element.tagName() == "shadowalign")
2946 {
2947 QString align = getFirstText(element).toLower();
2949 }
2950 else if (element.tagName() == "scrollstyle")
2951 {
2952 QString layout = getFirstText(element).toLower();
2953
2954 if (layout == "center")
2956 else if (layout == "groupcenter")
2958 else if (layout == "free")
2960 }
2961 else if (element.tagName() == "wrapstyle")
2962 {
2963 QString wrapstyle = getFirstText(element).toLower();
2964
2965 if (wrapstyle == "captive")
2967 else if (wrapstyle == "none")
2969 else if (wrapstyle == "selection")
2971 else if (wrapstyle == "flowing")
2973 else if (wrapstyle == "items")
2975 }
2976 else if (element.tagName() == "showarrow")
2977 {
2978 m_showArrow = parseBool(element);
2979 }
2980 else if (element.tagName() == "showscrollbar")
2981 {
2982 m_showScrollBar = parseBool(element);
2983 }
2984 else if (element.tagName() == "spacing")
2985 {
2986 m_itemHorizSpacing = NormX(getFirstText(element).toInt());
2987 m_itemVertSpacing = NormY(getFirstText(element).toInt());
2988 }
2989 else if (element.tagName() == "drawfrombottom")
2990 {
2992
2994 m_defaultAlignment |= Qt::AlignBottom;
2995 }
2996 else if (element.tagName() == "shadowdrawfrombottom")
2997 {
2999
3001 m_shadowAlignment = m_shadowAlignment.value_or(0) | Qt::AlignBottom;
3002 }
3003 else if (element.tagName() == "searchposition")
3004 {
3005 m_searchPosition = parsePoint(element);
3006 }
3007 else if (element.tagName() == "triggerevent")
3008 {
3009 QString trigger = getFirstText(element);
3010 if (!trigger.isEmpty())
3011 {
3012 QString action = element.attribute("action", "");
3013 if (action.isEmpty())
3014 {
3015 m_actionRemap[trigger] = "";
3016 }
3017 else
3018 {
3019 QString context = element.attribute("context", "");
3020 QString keylist = MythMainWindow::GetKey(context, action);
3021 QStringList keys = keylist.split(',', Qt::SkipEmptyParts);
3022 if (!keys.empty())
3023 m_actionRemap[trigger] = keys[0];
3024 }
3025 }
3026 }
3027 else
3028 {
3029 return MythUIType::ParseElement(filename, element, showWarnings);
3030 }
3031
3032 return true;
3033}
3034
3038void MythUIButtonList::DrawSelf(MythPainter * /*p*/, int /*xoffset*/, int /*yoffset*/,
3039 int /*alphaMod*/, QRect /*clipRect*/)
3040{
3041 if (m_needsUpdate)
3042 {
3045 }
3046}
3047
3052{
3053 auto *lb = new MythUIButtonList(parent, objectName());
3054 lb->CopyFrom(this);
3055}
3056
3061{
3062 auto *lb = dynamic_cast<MythUIButtonList *>(base);
3063 if (!lb)
3064 return;
3065
3066 m_layout = lb->m_layout;
3067 m_arrange = lb->m_arrange;
3068 m_defaultAlignment = lb->m_defaultAlignment;
3069 m_shadowAlignment = lb->m_shadowAlignment;
3070
3071 m_contentsRect = lb->m_contentsRect;
3072
3073 m_itemHeight = lb->m_itemHeight;
3074 m_itemWidth = lb->m_itemWidth;
3075 m_itemHorizSpacing = lb->m_itemHorizSpacing;
3076 m_itemVertSpacing = lb->m_itemVertSpacing;
3077 m_itemsVisible = lb->m_itemsVisible;
3078 m_maxVisible = lb->m_maxVisible;
3079
3080 m_active = lb->m_active;
3081 m_showArrow = lb->m_showArrow;
3082 m_showScrollBar = lb->m_showScrollBar;
3083
3084 m_defaultDrawFromBottom = lb->m_defaultDrawFromBottom;
3085 m_shadowDrawFromBottom = lb->m_shadowDrawFromBottom;
3086
3087 m_scrollStyle = lb->m_scrollStyle;
3088 m_wrapStyle = lb->m_wrapStyle;
3089
3090 m_clearing = false;
3092
3093 m_searchPosition = lb->m_searchPosition;
3094 m_searchFields = lb->m_searchFields;
3095
3097
3098 m_upArrow = dynamic_cast<MythUIStateType *>(GetChild("upscrollarrow"));
3099 m_downArrow = dynamic_cast<MythUIStateType *>(GetChild("downscrollarrow"));
3100 m_scrollBar = dynamic_cast<MythUIScrollBar *>(GetChild("scrollbar"));
3101
3102 for (int i = 0; i < m_itemsVisible; ++i)
3103 {
3104 QString name = QString("buttonlist button %1").arg(i);
3105 DeleteChild(name);
3106 }
3107
3108 m_buttonList.clear();
3109
3110 m_actionRemap = lb->m_actionRemap;
3111
3112 m_initialized = false;
3113}
3114
3119{
3121}
3122
3123void MythUIButtonList::SetLCDTitles(const QString &title, const QString &columnList)
3124{
3125 m_lcdTitle = title;
3126 m_lcdColumns = columnList.split('|');
3127}
3128
3130{
3131 if (!m_hasFocus)
3132 return;
3133
3134 LCD *lcddev = LCD::Get();
3135
3136 if (lcddev == nullptr)
3137 return;
3138
3139 // Build a list of the menu items
3140 QList<LCDMenuItem> menuItems;
3141
3142 auto start = std::max(0, m_selPosition - lcddev->getLCDHeight());
3143 auto end = std::min(m_itemCount, start + (lcddev->getLCDHeight() * 2));
3144
3145 menuItems.reserve(end - start);
3146 for (int r = start; r < end; ++r)
3147 {
3148 bool selected = r == GetCurrentPos();
3149
3152
3153 if (item->checkable())
3155
3156 QString text;
3157
3158 for (int x = 0; x < m_lcdColumns.count(); ++x)
3159 {
3160 if (!m_lcdColumns[x].isEmpty() && item->m_strings.contains(m_lcdColumns[x]))
3161 {
3162 // named text column
3163 TextProperties props = item->m_strings[m_lcdColumns[x]];
3164
3165 if (text.isEmpty())
3166 text = props.text;
3167 else
3168 text += " ~ " + props.text;
3169 }
3170 else
3171 {
3172 // default text column
3173 if (text.isEmpty())
3174 text = item->GetText();
3175 else
3176 text += " ~ " + item->GetText();
3177 }
3178 }
3179
3180 if (!text.isEmpty())
3181 menuItems.append(LCDMenuItem(selected, state, text));
3182 else
3183 menuItems.append(LCDMenuItem(selected, state, item->GetText()));
3184 }
3185
3186 if (!menuItems.isEmpty())
3187 lcddev->switchToMenu(menuItems, m_lcdTitle);
3188}
3189
3191{
3192 MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack");
3193
3194 auto *dlg = new SearchButtonListDialog(popupStack, "MythSearchListDialog", this, "");
3195
3196 if (dlg->Create())
3197 {
3198 if (m_searchPosition.x() != -2 || m_searchPosition.y() != -2)
3199 {
3200 int x = m_searchPosition.x();
3201 int y = m_searchPosition.y();
3202 QRect screenArea = GetMythMainWindow()->GetUIScreenRect();
3203 QRect dialogArea = dlg->GetArea();
3204
3205 if (x == -1)
3206 x = (screenArea.width() - dialogArea.width()) / 2;
3207
3208 if (y == -1)
3209 y = (screenArea.height() - dialogArea.height()) / 2;
3210
3211 dlg->SetPosition(x, y);
3212 }
3213
3214 popupStack->AddScreen(dlg);
3215 }
3216 else
3217 {
3218 delete dlg;
3219 }
3220}
3221
3222bool MythUIButtonList::Find(const QString &searchStr, bool startsWith)
3223{
3224 m_searchStr = searchStr;
3225 m_searchStartsWith = startsWith;
3226 return DoFind(false, true);
3227}
3228
3230{
3231 return DoFind(true, true);
3232}
3233
3235{
3236 return DoFind(true, false);
3237}
3238
3239bool MythUIButtonList::DoFind(bool doMove, bool searchForward)
3240{
3241 if (m_searchStr.isEmpty())
3242 return true;
3243
3244 if (GetCount() == 0)
3245 return false;
3246
3247 int startPos = GetCurrentPos();
3248 int currPos = startPos;
3249 bool found = false;
3250
3251 if (doMove)
3252 {
3253 if (searchForward)
3254 {
3255 ++currPos;
3256
3257 if (currPos >= GetCount())
3258 currPos = 0;
3259 }
3260 else
3261 {
3262 --currPos;
3263
3264 if (currPos < 0)
3265 currPos = GetCount() - 1;
3266 }
3267 }
3268
3269 while (true)
3270 {
3272
3273 if (found)
3274 {
3275 SetItemCurrent(currPos);
3276 return true;
3277 }
3278
3279 if (searchForward)
3280 {
3281 ++currPos;
3282
3283 if (currPos >= GetCount())
3284 currPos = 0;
3285 }
3286 else
3287 {
3288 --currPos;
3289
3290 if (currPos < 0)
3291 currPos = GetCount() - 1;
3292 }
3293
3294 if (startPos == currPos)
3295 break;
3296 }
3297
3298 return false;
3299}
3300
3302
3304 QString text, QString image,
3305 bool checkable, CheckState state,
3306 bool showArrow, int listPosition)
3307 : m_parent(lbtype), m_text(std::move(text)), m_imageFilename(std::move(image)),
3308 m_checkable(checkable), m_state(state), m_showArrow(showArrow)
3309{
3310 if (!lbtype)
3311 LOG(VB_GENERAL, LOG_ERR, "Cannot add a button to a non-existent list!");
3312
3313 if (state >= NotChecked)
3314 m_checkable = true;
3315
3316 if (m_parent)
3317 m_parent->InsertItem(this, listPosition);
3318}
3319
3321 const QString &text,
3322 QVariant data, int listPosition)
3323{
3324 if (!lbtype)
3325 LOG(VB_GENERAL, LOG_ERR, "Cannot add a button to a non-existent list!");
3326
3327 m_parent = lbtype;
3328 m_text = text;
3329 m_data = std::move(data);
3330
3331 m_image = nullptr;
3332
3333 m_checkable = false;
3335 m_showArrow = false;
3336 m_isVisible = false;
3337 m_enabled = true;
3338
3339 if (m_parent)
3340 m_parent->InsertItem(this, listPosition);
3341}
3342
3344{
3345 if (m_parent)
3346 m_parent->RemoveItem(this);
3347
3348 if (m_image)
3349 m_image->DecrRef();
3350
3351 QMap<QString, MythImage*>::iterator it;
3352 for (it = m_images.begin(); it != m_images.end(); ++it)
3353 {
3354 if (*it)
3355 (*it)->DecrRef();
3356 }
3357 m_images.clear();
3358}
3359
3360void MythUIButtonListItem::SetText(const QString &text, const QString &name,
3361 const QString &state)
3362{
3363 if (!name.isEmpty())
3364 {
3365 TextProperties textprop;
3366 textprop.text = text;
3367 textprop.state = state;
3368 m_strings.insert(name, textprop);
3369 }
3370 else
3371 {
3372 m_text = text;
3373 }
3374
3375 if (m_parent && m_isVisible)
3376 m_parent->Update();
3377}
3378
3380 const QString &state)
3381{
3382 InfoMap::const_iterator map_it = infoMap.begin();
3383
3384 while (map_it != infoMap.end())
3385 {
3386 TextProperties textprop;
3387 textprop.text = (*map_it);
3388 textprop.state = state;
3389 m_strings[map_it.key()] = textprop;
3390 ++map_it;
3391 }
3392
3393 if (m_parent && m_isVisible)
3394 m_parent->Update();
3395}
3396
3397void MythUIButtonListItem::SetTextFromMap(const QMap<QString, TextProperties> &stringMap)
3398{
3399 m_strings.clear();
3400 m_strings = stringMap;
3401}
3402
3404{
3405 m_textCb.fn = fn;
3406 m_textCb.data = data;
3407}
3408
3409QString MythUIButtonListItem::GetText(const QString &name) const
3410{
3411 if (name.isEmpty())
3412 return m_text;
3413 if (m_textCb.fn != nullptr)
3414 {
3415 QString result = m_textCb.fn(name, m_textCb.data);
3416 if (!result.isEmpty())
3417 return result;
3418 }
3419 if (m_strings.contains(name))
3420 return m_strings[name].text;
3421 return {};
3422}
3423
3425{
3426 if (name.isEmpty())
3427 return {.text=m_text, .state=""};
3428 if (m_textCb.fn != nullptr)
3429 {
3430 QString result = m_textCb.fn(name, m_textCb.data);
3431 if (!result.isEmpty())
3432 return {.text=result, .state=""};
3433 }
3434 if (m_strings.contains(name))
3435 return m_strings[name];
3436 return {};
3437}
3438
3439bool MythUIButtonListItem::FindText(const QString &searchStr, const QString &fieldList,
3440 bool startsWith) const
3441{
3442 if (fieldList.isEmpty())
3443 {
3444 if (startsWith)
3445 return m_text.startsWith(searchStr, Qt::CaseInsensitive);
3446 return m_text.contains(searchStr, Qt::CaseInsensitive);
3447 }
3448 if (fieldList == "**ALL**")
3449 {
3450 if (startsWith)
3451 {
3452 if (m_text.startsWith(searchStr, Qt::CaseInsensitive))
3453 return true;
3454 }
3455 else
3456 {
3457 if (m_text.contains(searchStr, Qt::CaseInsensitive))
3458 return true;
3459 }
3460
3461 QMap<QString, TextProperties>::const_iterator i = m_strings.constBegin();
3462
3463 while (i != m_strings.constEnd())
3464 {
3465 if (startsWith)
3466 {
3467 if (i.value().text.startsWith(searchStr, Qt::CaseInsensitive))
3468 return true;
3469 }
3470 else
3471 {
3472 if (i.value().text.contains(searchStr, Qt::CaseInsensitive))
3473 return true;
3474 }
3475
3476 ++i;
3477 }
3478 }
3479 else
3480 {
3481 QStringList fields = fieldList.split(',', Qt::SkipEmptyParts);
3482 for (int x = 0; x < fields.count(); ++x)
3483 {
3484 if (m_strings.contains(fields.at(x).trimmed()))
3485 {
3486 if (startsWith)
3487 {
3488 if (m_strings[fields.at(x)].text.startsWith(searchStr, Qt::CaseInsensitive))
3489 return true;
3490 }
3491 else
3492 {
3493 if (m_strings[fields.at(x)].text.contains(searchStr, Qt::CaseInsensitive))
3494 return true;
3495 }
3496 }
3497 }
3498 }
3499
3500 return false;
3501}
3502
3503void MythUIButtonListItem::SetFontState(const QString &state,
3504 const QString &name)
3505{
3506 if (!name.isEmpty())
3507 {
3508 if (m_strings.contains(name))
3509 m_strings[name].state = state;
3510 }
3511 else
3512 {
3514 }
3515
3516 if (m_parent && m_isVisible)
3517 m_parent->Update();
3518}
3519
3520void MythUIButtonListItem::SetImage(MythImage *image, const QString &name)
3521{
3522 if (image)
3523 image->IncrRef();
3524
3525 if (!name.isEmpty())
3526 {
3527 QMap<QString, MythImage*>::iterator it = m_images.find(name);
3528 if (it != m_images.end())
3529 {
3530 (*it)->DecrRef();
3531 if (image)
3532 *it = image;
3533 else
3534 m_images.erase(it);
3535 }
3536 else if (image)
3537 {
3538 m_images[name] = image;
3539 }
3540 }
3541 else
3542 {
3543 if (m_image)
3544 m_image->DecrRef();
3545 m_image = image;
3546 }
3547
3548 if (m_parent && m_isVisible)
3549 m_parent->Update();
3550}
3551
3553{
3554 m_imageFilenames.clear();
3555 m_imageFilenames = imageMap;
3556}
3557
3559{
3560 m_imageCb.fn = fn;
3561 m_imageCb.data = data;
3562}
3563
3565{
3566 if (!name.isEmpty())
3567 {
3568 QMap<QString, MythImage*>::iterator it = m_images.find(name);
3569 if (it != m_images.end())
3570 {
3571 (*it)->IncrRef();
3572 return (*it);
3573 }
3574 }
3575 else if (m_image)
3576 {
3577 m_image->IncrRef();
3578 return m_image;
3579 }
3580
3581 return nullptr;
3582}
3583
3585 const QString &filename, const QString &name, bool force_reload)
3586{
3587 bool do_update = force_reload;
3588
3589 if (!name.isEmpty())
3590 {
3591 InfoMap::iterator it = m_imageFilenames.find(name);
3592
3593 if (it == m_imageFilenames.end())
3594 {
3595 m_imageFilenames.insert(name, filename);
3596 do_update = true;
3597 }
3598 else if (*it != filename)
3599 {
3600 *it = filename;
3601 do_update = true;
3602 }
3603 }
3604 else if (m_imageFilename != filename)
3605 {
3607 do_update = true;
3608 }
3609
3610 if (m_parent && do_update && m_isVisible)
3611 m_parent->Update();
3612}
3613
3614QString MythUIButtonListItem::GetImageFilename(const QString &name) const
3615{
3616 if (name.isEmpty())
3617 return m_imageFilename;
3618
3619 if (m_imageCb.fn != nullptr)
3620 {
3621 QString result = m_imageCb.fn(name, m_imageCb.data);
3622 if (!result.isEmpty())
3623 return result;
3624 }
3625
3626 InfoMap::const_iterator it = m_imageFilenames.find(name);
3627
3628 if (it != m_imageFilenames.end())
3629 return *it;
3630
3631 return {};
3632}
3633
3634void MythUIButtonListItem::SetProgress1(int start, int total, int used)
3635{
3636 m_progress1.used = used;
3637 m_progress1.start = start;
3638 m_progress1.total = total;
3639
3640 if (m_parent && m_isVisible)
3641 m_parent->Update();
3642}
3643
3644void MythUIButtonListItem::SetProgress2(int start, int total, int used)
3645{
3646 m_progress2.used = used;
3647 m_progress2.start = start;
3648 m_progress2.total = total;
3649
3650 if (m_parent && m_isVisible)
3651 m_parent->Update();
3652}
3653
3654void MythUIButtonListItem::DisplayState(const QString &state,
3655 const QString &name)
3656{
3657 if (name.isEmpty())
3658 return;
3659
3660 bool do_update = false;
3661 InfoMap::iterator it = m_states.find(name);
3662
3663 if (it == m_states.end())
3664 {
3665 m_states.insert(name, state);
3666 do_update = true;
3667 }
3668 else if (*it != state)
3669 {
3670 *it = state;
3671 do_update = true;
3672 }
3673
3674 if (m_parent && do_update && m_isVisible)
3675 m_parent->Update();
3676}
3677
3679{
3680 m_states.clear();
3681 m_states = stateMap;
3682}
3683
3685{
3686 m_stateCb.fn = fn;
3687 m_stateCb.data = data;
3688}
3689
3690QString MythUIButtonListItem::GetState(const QString &name)
3691{
3692 if (name.isEmpty())
3693 return {};
3694 if (m_stateCb.fn != nullptr)
3695 {
3696 QString result = m_stateCb.fn(name, m_textCb.data);
3697 if (!result.isEmpty())
3698 return result;
3699 }
3700 if (m_states.contains(name))
3701 return m_states[name];
3702 return {};
3703}
3704
3706{
3707 return m_checkable;
3708}
3709
3711{
3712 return m_state;
3713}
3714
3716{
3717 return m_parent;
3718}
3719
3721{
3722 if (!m_checkable || m_state == state)
3723 return;
3724
3725 m_state = state;
3726
3727 if (m_parent && m_isVisible)
3728 m_parent->Update();
3729}
3730
3732{
3733 m_checkable = flag;
3734}
3735
3737{
3738 m_showArrow = flag;
3739}
3740
3742{
3743 return m_enabled;
3744}
3745
3747{
3748 m_enabled = flag;
3749}
3750
3752{
3753 m_data = std::move(data);
3754}
3755
3757{
3758 return m_data;
3759}
3760
3762{
3763 if (m_parent)
3764 return m_parent->MoveItemUpDown(this, flag);
3765 return false;
3766}
3767
3769{
3770 if (!buttontext)
3771 return;
3772
3773 buttontext->SetText(m_text);
3774 buttontext->SetFontState(m_fontState);
3775}
3776
3778{
3779 if (!buttonimage)
3780 return;
3781
3782 if (!m_imageFilename.isEmpty())
3783 {
3784 buttonimage->SetFilename(m_imageFilename);
3785 buttonimage->Load();
3786 }
3787 else if (m_image)
3788 {
3789 buttonimage->SetImage(m_image);
3790 }
3791}
3792
3794{
3795 if (!buttonarrow)
3796 return;
3797 buttonarrow->SetVisible(m_showArrow);
3798}
3799
3801{
3802 if (!buttoncheck)
3803 return;
3804
3805 buttoncheck->SetVisible(m_checkable);
3806
3807 if (!m_checkable)
3808 return;
3809
3810 if (m_state == NotChecked)
3811 buttoncheck->DisplayState(MythUIStateType::Off);
3812 else if (m_state == HalfChecked)
3813 buttoncheck->DisplayState(MythUIStateType::Half);
3814 else
3815 buttoncheck->DisplayState(MythUIStateType::Full);
3816}
3817
3819{
3820 if (!buttonprogress)
3821 return;
3822
3824}
3825
3827{
3828 if (!buttonprogress)
3829 return;
3830
3832}
3833
3835 const TextProperties& textprop)
3836{
3837 if (!text)
3838 return;
3839
3840 QString newText = text->GetTemplateText();
3841
3842 static const QRegularExpression re {R"(%(([^\|%]+)?\||\|(.))?([\w#]+)(\|(.+?))?%)",
3843 QRegularExpression::DotMatchesEverythingOption};
3844
3845 if (!newText.isEmpty() && newText.contains(re))
3846 {
3847 QString tempString = newText;
3848
3849 QRegularExpressionMatchIterator i = re.globalMatch(newText);
3850 while (i.hasNext()) {
3851 QRegularExpressionMatch match = i.next();
3852 QString key = match.captured(4).toLower().trimmed();
3853 QString replacement;
3854 QString value = GetText(key);
3855
3856 if (!value.isEmpty())
3857 {
3858 replacement = QString("%1%2%3%4")
3859 .arg(match.captured(2),
3860 match.captured(3),
3861 value,
3862 match.captured(6));
3863 }
3864
3865 tempString.replace(match.captured(0), replacement);
3866 }
3867
3868 newText = tempString;
3869 }
3870 else
3871 {
3872 newText = textprop.text;
3873 }
3874
3875 if (newText.isEmpty())
3876 text->Reset();
3877 else
3878 text->SetText(newText);
3879
3880 text->SetFontState(textprop.state.isEmpty() ? m_fontState : textprop.state);
3881}
3882
3884{
3885 if (!image)
3886 return;
3887
3888 if (!filename.isEmpty())
3889 {
3890 image->SetFilename(filename);
3891 image->Load();
3892 }
3893 else
3894 {
3895 image->Reset();
3896 }
3897}
3898
3900{
3901 if (!uiimage)
3902 return;
3903
3904 if (image)
3905 uiimage->SetImage(image);
3906 else
3907 uiimage->Reset();
3908}
3909
3911{
3912 if (!statetype)
3913 return;
3914
3915 if (!statetype->DisplayState(name))
3916 statetype->Reset();
3917}
3918
3920 bool selected)
3921{
3922 if (!m_parent)
3923 return;
3924
3925 m_parent->ItemVisible(this);
3926 m_isVisible = true;
3927
3928 QString state;
3929
3930 if (!m_parent->IsEnabled())
3931 {
3932 state = "disabled";
3933 }
3934 else if (!m_enabled)
3935 {
3936 state = m_parent->m_active ? "disabledactive" : "disabledinactive";
3937 }
3938 else if (selected)
3939 {
3940 button->MoveToTop();
3941 state = m_parent->m_active ? "selectedactive" : "selectedinactive";
3942 }
3943 else
3944 {
3945 state = m_parent->m_active ? "active" : "inactive";
3946 }
3947
3948 if (m_parent->IsShadowing())
3949 {
3950 if (state == "inactive" && button->GetState("shadow"))
3951 state = "shadow";
3952 else if (state == "selectedinactive" &&
3953 button->GetState("selectedshadow"))
3954 state = "selectedshadow";
3955 }
3956
3957 // Begin compatibility code
3958 // Attempt to fallback if the theme is missing certain states
3959 if (state == "disabled" && !button->GetState(state))
3960 {
3961 LOG(VB_GUI, LOG_WARNING, "Theme Error: Missing buttonlist state: disabled");
3962 state = "inactive";
3963 }
3964
3965 if (state == "inactive" && !button->GetState(state))
3966 {
3967 LOG(VB_GUI, LOG_WARNING, "Theme Error: Missing buttonlist state: inactive");
3968 state = "active";
3969 }
3970 // End compatibility code
3971
3972 auto *buttonstate = dynamic_cast<MythUIGroup *>(button->GetState(state));
3973 if (!buttonstate)
3974 {
3975 LOG(VB_GENERAL, LOG_CRIT, QString("Theme Error: Missing buttonlist state: %1")
3976 .arg(state));
3977 return;
3978 }
3979
3980 buttonstate->Reset();
3981
3982 QList<MythUIType *> descendants = buttonstate->GetAllDescendants();
3983 for (MythUIType *obj : std::as_const(descendants))
3984 {
3985 QString name = obj->objectName();
3986 if (name == "buttontext")
3987 DoButtonText(dynamic_cast<MythUIText *>(obj));
3988 else if (name == "buttonimage")
3989 DoButtonImage(dynamic_cast<MythUIImage *>(obj));
3990 else if (name == "buttonarrow")
3991 DoButtonArrow(dynamic_cast<MythUIImage *>(obj));
3992 else if (name == "buttoncheck")
3993 DoButtonCheck(dynamic_cast<MythUIStateType *>(obj));
3994 else if (name == "buttonprogress1")
3995 DoButtonProgress1(dynamic_cast<MythUIProgressBar *>(obj));
3996 else if (name == "buttonprogress2")
3997 DoButtonProgress2(dynamic_cast<MythUIProgressBar *>(obj));
3998
3999 TextProperties textprop = GetTextProp(name);
4000 if (!textprop.text.isEmpty())
4001 DoButtonLookupText(dynamic_cast<MythUIText *>(obj), textprop);
4002
4003 QString filename = GetImageFilename(name);
4004 if (!filename.isEmpty())
4005 DoButtonLookupFilename (dynamic_cast<MythUIImage *>(obj), filename);
4006
4007 if (m_images.contains(name))
4008 DoButtonLookupImage(dynamic_cast<MythUIImage *>(obj), m_images[name]);
4009
4010 QString luState = GetState(name);
4011 if (!luState.isEmpty())
4012 DoButtonLookupState(dynamic_cast<MythUIStateType *>(obj), luState);
4013 }
4014
4015 // There is no need to check the return value here, since we already
4016 // checked that the state exists with GetState() earlier
4017 button->DisplayState(state);
4018}
4019
4020//---------------------------------------------------------
4021// SearchButtonListDialog
4022//---------------------------------------------------------
4024{
4025 if (!CopyWindowFromBase("MythSearchListDialog", this))
4026 return false;
4027
4028 bool err = false;
4029 UIUtilE::Assign(this, m_searchEdit, "searchedit", &err);
4030 UIUtilE::Assign(this, m_prevButton, "prevbutton", &err);
4031 UIUtilE::Assign(this, m_nextButton, "nextbutton", &err);
4032 UIUtilW::Assign(this, m_searchState, "searchstate");
4033
4034 if (err)
4035 {
4036 LOG(VB_GENERAL, LOG_ERR, "Cannot load screen 'MythSearchListDialog'");
4037 return false;
4038 }
4039
4041
4045
4047
4048 return true;
4049}
4050
4052{
4053 if (GetFocusWidget() && GetFocusWidget()->keyPressEvent(event))
4054 return true;
4055
4056 QStringList actions;
4057 bool handled = GetMythMainWindow()->TranslateKeyPress("Global", event, actions, false);
4058
4059 for (int i = 0; i < actions.size() && !handled; ++i)
4060 {
4061 const QString& action = actions[i];
4062 handled = true;
4063
4064 if (action == "0")
4065 {
4067 searchChanged();
4068 }
4069 else
4070 {
4071 handled = false;
4072 }
4073 }
4074
4075 if (!handled && MythScreenType::keyPressEvent(event))
4076 handled = true;
4077
4078 return handled;
4079}
4080
4082{
4084
4085 if (m_searchState)
4086 m_searchState->DisplayState(found ? "found" : "notfound");
4087}
4088
4090{
4091 bool found = m_parentList->FindNext();
4092
4093 if (m_searchState)
4094 m_searchState->DisplayState(found ? "found" : "notfound");
4095}
4096
4098{
4099 bool found = m_parentList->FindPrev();
4100
4101 if (m_searchState)
4102 m_searchState->DisplayState(found ? "found" : "notfound");
4103}
4104
4106{
4108 return;
4109
4110 int maximum = (m_itemCount <= m_itemsVisible) ? 0 : m_itemCount;
4111 m_scrollBar->SetMaximum(maximum);
4115}
4116
4117#include "moc_mythuibuttonlist.cpp"
Definition: lcddevice.h:170
static LCD * Get(void)
Definition: lcddevice.cpp:68
void switchToMenu(QList< LCDMenuItem > &menuItems, const QString &app_name="", bool popMenu=true)
Definition: lcddevice.cpp:589
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:99
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:51
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:97
bool IsEnabled(void) const
Definition: mythuitype.h:130
bool m_enableInitiator
Definition: mythuitype.h:278
void SetCanTakeFocus(bool set=true)
Set whether this widget can take focus.
Definition: mythuitype.cpp:348
bool m_initiator
Definition: mythuitype.h:279
void TakingFocus(void)
void RequestUpdate(void)
virtual void SetVisible(bool visible)
QString GetXMLLocation(void) const
Definition: mythuitype.h:193
void Disabling(void)
MythPoint m_minSize
Definition: mythuitype.h:290
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:227
void SetRedraw(void)
Definition: mythuitype.cpp:299
void Enabling(void)
virtual void SetMinArea(const MythRect &rect)
Set the minimum area based on the given size.
Definition: mythuitype.cpp:806
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:871
MythUIType * m_parent
Definition: mythuitype.h:308
virtual MythRect GetFullArea(void) const
Definition: mythuitype.cpp:879
bool m_hasFocus
Definition: mythuitype.h:275
void SetPosition(int x, int y)
Convenience method, calls SetPosition(const MythPoint&) Override that instead to change functionality...
Definition: mythuitype.cpp:519
MythUIType * GetChild(const QString &name) const
Get a named child of this UIType.
Definition: mythuitype.cpp:130
bool MoveToTop(void)
void DeleteChild(const QString &name)
Delete a named child of this UIType.
Definition: mythuitype.cpp:145
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:71
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:288
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:60
static pid_list_t::iterator find(const PIDInfoMap &map, pid_list_t &list, pid_list_t::iterator begin, pid_list_t::iterator end, bool find_open)
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
static bool Assign(ContainerType *container, UIType *&item, const QString &name, bool *err=nullptr)
Definition: mythuiutils.h:27