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 for (int r = start; r < end; ++r)
3146 {
3147 bool selected = r == GetCurrentPos();
3148
3151
3152 if (item->checkable())
3154
3155 QString text;
3156
3157 for (int x = 0; x < m_lcdColumns.count(); ++x)
3158 {
3159 if (!m_lcdColumns[x].isEmpty() && item->m_strings.contains(m_lcdColumns[x]))
3160 {
3161 // named text column
3162 TextProperties props = item->m_strings[m_lcdColumns[x]];
3163
3164 if (text.isEmpty())
3165 text = props.text;
3166 else
3167 text += " ~ " + props.text;
3168 }
3169 else
3170 {
3171 // default text column
3172 if (text.isEmpty())
3173 text = item->GetText();
3174 else
3175 text += " ~ " + item->GetText();
3176 }
3177 }
3178
3179 if (!text.isEmpty())
3180 menuItems.append(LCDMenuItem(selected, state, text));
3181 else
3182 menuItems.append(LCDMenuItem(selected, state, item->GetText()));
3183 }
3184
3185 if (!menuItems.isEmpty())
3186 lcddev->switchToMenu(menuItems, m_lcdTitle);
3187}
3188
3190{
3191 MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack");
3192
3193 auto *dlg = new SearchButtonListDialog(popupStack, "MythSearchListDialog", this, "");
3194
3195 if (dlg->Create())
3196 {
3197 if (m_searchPosition.x() != -2 || m_searchPosition.y() != -2)
3198 {
3199 int x = m_searchPosition.x();
3200 int y = m_searchPosition.y();
3201 QRect screenArea = GetMythMainWindow()->GetUIScreenRect();
3202 QRect dialogArea = dlg->GetArea();
3203
3204 if (x == -1)
3205 x = (screenArea.width() - dialogArea.width()) / 2;
3206
3207 if (y == -1)
3208 y = (screenArea.height() - dialogArea.height()) / 2;
3209
3210 dlg->SetPosition(x, y);
3211 }
3212
3213 popupStack->AddScreen(dlg);
3214 }
3215 else
3216 {
3217 delete dlg;
3218 }
3219}
3220
3221bool MythUIButtonList::Find(const QString &searchStr, bool startsWith)
3222{
3223 m_searchStr = searchStr;
3224 m_searchStartsWith = startsWith;
3225 return DoFind(false, true);
3226}
3227
3229{
3230 return DoFind(true, true);
3231}
3232
3234{
3235 return DoFind(true, false);
3236}
3237
3238bool MythUIButtonList::DoFind(bool doMove, bool searchForward)
3239{
3240 if (m_searchStr.isEmpty())
3241 return true;
3242
3243 if (GetCount() == 0)
3244 return false;
3245
3246 int startPos = GetCurrentPos();
3247 int currPos = startPos;
3248 bool found = false;
3249
3250 if (doMove)
3251 {
3252 if (searchForward)
3253 {
3254 ++currPos;
3255
3256 if (currPos >= GetCount())
3257 currPos = 0;
3258 }
3259 else
3260 {
3261 --currPos;
3262
3263 if (currPos < 0)
3264 currPos = GetCount() - 1;
3265 }
3266 }
3267
3268 while (true)
3269 {
3271
3272 if (found)
3273 {
3274 SetItemCurrent(currPos);
3275 return true;
3276 }
3277
3278 if (searchForward)
3279 {
3280 ++currPos;
3281
3282 if (currPos >= GetCount())
3283 currPos = 0;
3284 }
3285 else
3286 {
3287 --currPos;
3288
3289 if (currPos < 0)
3290 currPos = GetCount() - 1;
3291 }
3292
3293 if (startPos == currPos)
3294 break;
3295 }
3296
3297 return false;
3298}
3299
3301
3303 QString text, QString image,
3304 bool checkable, CheckState state,
3305 bool showArrow, int listPosition)
3306 : m_parent(lbtype), m_text(std::move(text)), m_imageFilename(std::move(image)),
3307 m_checkable(checkable), m_state(state), m_showArrow(showArrow)
3308{
3309 if (!lbtype)
3310 LOG(VB_GENERAL, LOG_ERR, "Cannot add a button to a non-existent list!");
3311
3312 if (state >= NotChecked)
3313 m_checkable = true;
3314
3315 if (m_parent)
3316 m_parent->InsertItem(this, listPosition);
3317}
3318
3320 const QString &text,
3321 QVariant data, int listPosition)
3322{
3323 if (!lbtype)
3324 LOG(VB_GENERAL, LOG_ERR, "Cannot add a button to a non-existent list!");
3325
3326 m_parent = lbtype;
3327 m_text = text;
3328 m_data = std::move(data);
3329
3330 m_image = nullptr;
3331
3332 m_checkable = false;
3334 m_showArrow = false;
3335 m_isVisible = false;
3336 m_enabled = true;
3337
3338 if (m_parent)
3339 m_parent->InsertItem(this, listPosition);
3340}
3341
3343{
3344 if (m_parent)
3345 m_parent->RemoveItem(this);
3346
3347 if (m_image)
3348 m_image->DecrRef();
3349
3350 QMap<QString, MythImage*>::iterator it;
3351 for (it = m_images.begin(); it != m_images.end(); ++it)
3352 {
3353 if (*it)
3354 (*it)->DecrRef();
3355 }
3356 m_images.clear();
3357}
3358
3359void MythUIButtonListItem::SetText(const QString &text, const QString &name,
3360 const QString &state)
3361{
3362 if (!name.isEmpty())
3363 {
3364 TextProperties textprop;
3365 textprop.text = text;
3366 textprop.state = state;
3367 m_strings.insert(name, textprop);
3368 }
3369 else
3370 {
3371 m_text = text;
3372 }
3373
3374 if (m_parent && m_isVisible)
3375 m_parent->Update();
3376}
3377
3379 const QString &state)
3380{
3381 InfoMap::const_iterator map_it = infoMap.begin();
3382
3383 while (map_it != infoMap.end())
3384 {
3385 TextProperties textprop;
3386 textprop.text = (*map_it);
3387 textprop.state = state;
3388 m_strings[map_it.key()] = textprop;
3389 ++map_it;
3390 }
3391
3392 if (m_parent && m_isVisible)
3393 m_parent->Update();
3394}
3395
3396void MythUIButtonListItem::SetTextFromMap(const QMap<QString, TextProperties> &stringMap)
3397{
3398 m_strings.clear();
3399 m_strings = stringMap;
3400}
3401
3403{
3404 m_textCb.fn = fn;
3405 m_textCb.data = data;
3406}
3407
3408QString MythUIButtonListItem::GetText(const QString &name) const
3409{
3410 if (name.isEmpty())
3411 return m_text;
3412 if (m_textCb.fn != nullptr)
3413 {
3414 QString result = m_textCb.fn(name, m_textCb.data);
3415 if (!result.isEmpty())
3416 return result;
3417 }
3418 if (m_strings.contains(name))
3419 return m_strings[name].text;
3420 return {};
3421}
3422
3424{
3425 if (name.isEmpty())
3426 return {.text=m_text, .state=""};
3427 if (m_textCb.fn != nullptr)
3428 {
3429 QString result = m_textCb.fn(name, m_textCb.data);
3430 if (!result.isEmpty())
3431 return {.text=result, .state=""};
3432 }
3433 if (m_strings.contains(name))
3434 return m_strings[name];
3435 return {};
3436}
3437
3438bool MythUIButtonListItem::FindText(const QString &searchStr, const QString &fieldList,
3439 bool startsWith) const
3440{
3441 if (fieldList.isEmpty())
3442 {
3443 if (startsWith)
3444 return m_text.startsWith(searchStr, Qt::CaseInsensitive);
3445 return m_text.contains(searchStr, Qt::CaseInsensitive);
3446 }
3447 if (fieldList == "**ALL**")
3448 {
3449 if (startsWith)
3450 {
3451 if (m_text.startsWith(searchStr, Qt::CaseInsensitive))
3452 return true;
3453 }
3454 else
3455 {
3456 if (m_text.contains(searchStr, Qt::CaseInsensitive))
3457 return true;
3458 }
3459
3460 QMap<QString, TextProperties>::const_iterator i = m_strings.constBegin();
3461
3462 while (i != m_strings.constEnd())
3463 {
3464 if (startsWith)
3465 {
3466 if (i.value().text.startsWith(searchStr, Qt::CaseInsensitive))
3467 return true;
3468 }
3469 else
3470 {
3471 if (i.value().text.contains(searchStr, Qt::CaseInsensitive))
3472 return true;
3473 }
3474
3475 ++i;
3476 }
3477 }
3478 else
3479 {
3480 QStringList fields = fieldList.split(',', Qt::SkipEmptyParts);
3481 for (int x = 0; x < fields.count(); ++x)
3482 {
3483 if (m_strings.contains(fields.at(x).trimmed()))
3484 {
3485 if (startsWith)
3486 {
3487 if (m_strings[fields.at(x)].text.startsWith(searchStr, Qt::CaseInsensitive))
3488 return true;
3489 }
3490 else
3491 {
3492 if (m_strings[fields.at(x)].text.contains(searchStr, Qt::CaseInsensitive))
3493 return true;
3494 }
3495 }
3496 }
3497 }
3498
3499 return false;
3500}
3501
3502void MythUIButtonListItem::SetFontState(const QString &state,
3503 const QString &name)
3504{
3505 if (!name.isEmpty())
3506 {
3507 if (m_strings.contains(name))
3508 m_strings[name].state = state;
3509 }
3510 else
3511 {
3513 }
3514
3515 if (m_parent && m_isVisible)
3516 m_parent->Update();
3517}
3518
3519void MythUIButtonListItem::SetImage(MythImage *image, const QString &name)
3520{
3521 if (image)
3522 image->IncrRef();
3523
3524 if (!name.isEmpty())
3525 {
3526 QMap<QString, MythImage*>::iterator it = m_images.find(name);
3527 if (it != m_images.end())
3528 {
3529 (*it)->DecrRef();
3530 if (image)
3531 *it = image;
3532 else
3533 m_images.erase(it);
3534 }
3535 else if (image)
3536 {
3537 m_images[name] = image;
3538 }
3539 }
3540 else
3541 {
3542 if (m_image)
3543 m_image->DecrRef();
3544 m_image = image;
3545 }
3546
3547 if (m_parent && m_isVisible)
3548 m_parent->Update();
3549}
3550
3552{
3553 m_imageFilenames.clear();
3554 m_imageFilenames = imageMap;
3555}
3556
3558{
3559 m_imageCb.fn = fn;
3560 m_imageCb.data = data;
3561}
3562
3564{
3565 if (!name.isEmpty())
3566 {
3567 QMap<QString, MythImage*>::iterator it = m_images.find(name);
3568 if (it != m_images.end())
3569 {
3570 (*it)->IncrRef();
3571 return (*it);
3572 }
3573 }
3574 else if (m_image)
3575 {
3576 m_image->IncrRef();
3577 return m_image;
3578 }
3579
3580 return nullptr;
3581}
3582
3584 const QString &filename, const QString &name, bool force_reload)
3585{
3586 bool do_update = force_reload;
3587
3588 if (!name.isEmpty())
3589 {
3590 InfoMap::iterator it = m_imageFilenames.find(name);
3591
3592 if (it == m_imageFilenames.end())
3593 {
3594 m_imageFilenames.insert(name, filename);
3595 do_update = true;
3596 }
3597 else if (*it != filename)
3598 {
3599 *it = filename;
3600 do_update = true;
3601 }
3602 }
3603 else if (m_imageFilename != filename)
3604 {
3606 do_update = true;
3607 }
3608
3609 if (m_parent && do_update && m_isVisible)
3610 m_parent->Update();
3611}
3612
3613QString MythUIButtonListItem::GetImageFilename(const QString &name) const
3614{
3615 if (name.isEmpty())
3616 return m_imageFilename;
3617
3618 if (m_imageCb.fn != nullptr)
3619 {
3620 QString result = m_imageCb.fn(name, m_imageCb.data);
3621 if (!result.isEmpty())
3622 return result;
3623 }
3624
3625 InfoMap::const_iterator it = m_imageFilenames.find(name);
3626
3627 if (it != m_imageFilenames.end())
3628 return *it;
3629
3630 return {};
3631}
3632
3633void MythUIButtonListItem::SetProgress1(int start, int total, int used)
3634{
3635 m_progress1.used = used;
3636 m_progress1.start = start;
3637 m_progress1.total = total;
3638
3639 if (m_parent && m_isVisible)
3640 m_parent->Update();
3641}
3642
3643void MythUIButtonListItem::SetProgress2(int start, int total, int used)
3644{
3645 m_progress2.used = used;
3646 m_progress2.start = start;
3647 m_progress2.total = total;
3648
3649 if (m_parent && m_isVisible)
3650 m_parent->Update();
3651}
3652
3653void MythUIButtonListItem::DisplayState(const QString &state,
3654 const QString &name)
3655{
3656 if (name.isEmpty())
3657 return;
3658
3659 bool do_update = false;
3660 InfoMap::iterator it = m_states.find(name);
3661
3662 if (it == m_states.end())
3663 {
3664 m_states.insert(name, state);
3665 do_update = true;
3666 }
3667 else if (*it != state)
3668 {
3669 *it = state;
3670 do_update = true;
3671 }
3672
3673 if (m_parent && do_update && m_isVisible)
3674 m_parent->Update();
3675}
3676
3678{
3679 m_states.clear();
3680 m_states = stateMap;
3681}
3682
3684{
3685 m_stateCb.fn = fn;
3686 m_stateCb.data = data;
3687}
3688
3689QString MythUIButtonListItem::GetState(const QString &name)
3690{
3691 if (name.isEmpty())
3692 return {};
3693 if (m_stateCb.fn != nullptr)
3694 {
3695 QString result = m_stateCb.fn(name, m_textCb.data);
3696 if (!result.isEmpty())
3697 return result;
3698 }
3699 if (m_states.contains(name))
3700 return m_states[name];
3701 return {};
3702}
3703
3705{
3706 return m_checkable;
3707}
3708
3710{
3711 return m_state;
3712}
3713
3715{
3716 return m_parent;
3717}
3718
3720{
3721 if (!m_checkable || m_state == state)
3722 return;
3723
3724 m_state = state;
3725
3726 if (m_parent && m_isVisible)
3727 m_parent->Update();
3728}
3729
3731{
3732 m_checkable = flag;
3733}
3734
3736{
3737 m_showArrow = flag;
3738}
3739
3741{
3742 return m_enabled;
3743}
3744
3746{
3747 m_enabled = flag;
3748}
3749
3751{
3752 m_data = std::move(data);
3753}
3754
3756{
3757 return m_data;
3758}
3759
3761{
3762 if (m_parent)
3763 return m_parent->MoveItemUpDown(this, flag);
3764 return false;
3765}
3766
3768{
3769 if (!buttontext)
3770 return;
3771
3772 buttontext->SetText(m_text);
3773 buttontext->SetFontState(m_fontState);
3774}
3775
3777{
3778 if (!buttonimage)
3779 return;
3780
3781 if (!m_imageFilename.isEmpty())
3782 {
3783 buttonimage->SetFilename(m_imageFilename);
3784 buttonimage->Load();
3785 }
3786 else if (m_image)
3787 {
3788 buttonimage->SetImage(m_image);
3789 }
3790}
3791
3793{
3794 if (!buttonarrow)
3795 return;
3796 buttonarrow->SetVisible(m_showArrow);
3797}
3798
3800{
3801 if (!buttoncheck)
3802 return;
3803
3804 buttoncheck->SetVisible(m_checkable);
3805
3806 if (!m_checkable)
3807 return;
3808
3809 if (m_state == NotChecked)
3810 buttoncheck->DisplayState(MythUIStateType::Off);
3811 else if (m_state == HalfChecked)
3812 buttoncheck->DisplayState(MythUIStateType::Half);
3813 else
3814 buttoncheck->DisplayState(MythUIStateType::Full);
3815}
3816
3818{
3819 if (!buttonprogress)
3820 return;
3821
3823}
3824
3826{
3827 if (!buttonprogress)
3828 return;
3829
3831}
3832
3834 const TextProperties& textprop)
3835{
3836 if (!text)
3837 return;
3838
3839 QString newText = text->GetTemplateText();
3840
3841 static const QRegularExpression re {R"(%(([^\|%]+)?\||\|(.))?([\w#]+)(\|(.+?))?%)",
3842 QRegularExpression::DotMatchesEverythingOption};
3843
3844 if (!newText.isEmpty() && newText.contains(re))
3845 {
3846 QString tempString = newText;
3847
3848 QRegularExpressionMatchIterator i = re.globalMatch(newText);
3849 while (i.hasNext()) {
3850 QRegularExpressionMatch match = i.next();
3851 QString key = match.captured(4).toLower().trimmed();
3852 QString replacement;
3853 QString value = GetText(key);
3854
3855 if (!value.isEmpty())
3856 {
3857 replacement = QString("%1%2%3%4")
3858 .arg(match.captured(2),
3859 match.captured(3),
3860 value,
3861 match.captured(6));
3862 }
3863
3864 tempString.replace(match.captured(0), replacement);
3865 }
3866
3867 newText = tempString;
3868 }
3869 else
3870 {
3871 newText = textprop.text;
3872 }
3873
3874 if (newText.isEmpty())
3875 text->Reset();
3876 else
3877 text->SetText(newText);
3878
3879 text->SetFontState(textprop.state.isEmpty() ? m_fontState : textprop.state);
3880}
3881
3883{
3884 if (!image)
3885 return;
3886
3887 if (!filename.isEmpty())
3888 {
3889 image->SetFilename(filename);
3890 image->Load();
3891 }
3892 else
3893 {
3894 image->Reset();
3895 }
3896}
3897
3899{
3900 if (!uiimage)
3901 return;
3902
3903 if (image)
3904 uiimage->SetImage(image);
3905 else
3906 uiimage->Reset();
3907}
3908
3910{
3911 if (!statetype)
3912 return;
3913
3914 if (!statetype->DisplayState(name))
3915 statetype->Reset();
3916}
3917
3919 bool selected)
3920{
3921 if (!m_parent)
3922 return;
3923
3924 m_parent->ItemVisible(this);
3925 m_isVisible = true;
3926
3927 QString state;
3928
3929 if (!m_parent->IsEnabled())
3930 {
3931 state = "disabled";
3932 }
3933 else if (!m_enabled)
3934 {
3935 state = m_parent->m_active ? "disabledactive" : "disabledinactive";
3936 }
3937 else if (selected)
3938 {
3939 button->MoveToTop();
3940 state = m_parent->m_active ? "selectedactive" : "selectedinactive";
3941 }
3942 else
3943 {
3944 state = m_parent->m_active ? "active" : "inactive";
3945 }
3946
3947 if (m_parent->IsShadowing())
3948 {
3949 if (state == "inactive" && button->GetState("shadow"))
3950 state = "shadow";
3951 else if (state == "selectedinactive" &&
3952 button->GetState("selectedshadow"))
3953 state = "selectedshadow";
3954 }
3955
3956 // Begin compatibility code
3957 // Attempt to fallback if the theme is missing certain states
3958 if (state == "disabled" && !button->GetState(state))
3959 {
3960 LOG(VB_GUI, LOG_WARNING, "Theme Error: Missing buttonlist state: disabled");
3961 state = "inactive";
3962 }
3963
3964 if (state == "inactive" && !button->GetState(state))
3965 {
3966 LOG(VB_GUI, LOG_WARNING, "Theme Error: Missing buttonlist state: inactive");
3967 state = "active";
3968 }
3969 // End compatibility code
3970
3971 auto *buttonstate = dynamic_cast<MythUIGroup *>(button->GetState(state));
3972 if (!buttonstate)
3973 {
3974 LOG(VB_GENERAL, LOG_CRIT, QString("Theme Error: Missing buttonlist state: %1")
3975 .arg(state));
3976 return;
3977 }
3978
3979 buttonstate->Reset();
3980
3981 QList<MythUIType *> descendants = buttonstate->GetAllDescendants();
3982 for (MythUIType *obj : std::as_const(descendants))
3983 {
3984 QString name = obj->objectName();
3985 if (name == "buttontext")
3986 DoButtonText(dynamic_cast<MythUIText *>(obj));
3987 else if (name == "buttonimage")
3988 DoButtonImage(dynamic_cast<MythUIImage *>(obj));
3989 else if (name == "buttonarrow")
3990 DoButtonArrow(dynamic_cast<MythUIImage *>(obj));
3991 else if (name == "buttoncheck")
3992 DoButtonCheck(dynamic_cast<MythUIStateType *>(obj));
3993 else if (name == "buttonprogress1")
3994 DoButtonProgress1(dynamic_cast<MythUIProgressBar *>(obj));
3995 else if (name == "buttonprogress2")
3996 DoButtonProgress2(dynamic_cast<MythUIProgressBar *>(obj));
3997
3998 TextProperties textprop = GetTextProp(name);
3999 if (!textprop.text.isEmpty())
4000 DoButtonLookupText(dynamic_cast<MythUIText *>(obj), textprop);
4001
4002 QString filename = GetImageFilename(name);
4003 if (!filename.isEmpty())
4004 DoButtonLookupFilename (dynamic_cast<MythUIImage *>(obj), filename);
4005
4006 if (m_images.contains(name))
4007 DoButtonLookupImage(dynamic_cast<MythUIImage *>(obj), m_images[name]);
4008
4009 QString luState = GetState(name);
4010 if (!luState.isEmpty())
4011 DoButtonLookupState(dynamic_cast<MythUIStateType *>(obj), luState);
4012 }
4013
4014 // There is no need to check the return value here, since we already
4015 // checked that the state exists with GetState() earlier
4016 button->DisplayState(state);
4017}
4018
4019//---------------------------------------------------------
4020// SearchButtonListDialog
4021//---------------------------------------------------------
4023{
4024 if (!CopyWindowFromBase("MythSearchListDialog", this))
4025 return false;
4026
4027 bool err = false;
4028 UIUtilE::Assign(this, m_searchEdit, "searchedit", &err);
4029 UIUtilE::Assign(this, m_prevButton, "prevbutton", &err);
4030 UIUtilE::Assign(this, m_nextButton, "nextbutton", &err);
4031 UIUtilW::Assign(this, m_searchState, "searchstate");
4032
4033 if (err)
4034 {
4035 LOG(VB_GENERAL, LOG_ERR, "Cannot load screen 'MythSearchListDialog'");
4036 return false;
4037 }
4038
4040
4044
4046
4047 return true;
4048}
4049
4051{
4052 if (GetFocusWidget() && GetFocusWidget()->keyPressEvent(event))
4053 return true;
4054
4055 QStringList actions;
4056 bool handled = GetMythMainWindow()->TranslateKeyPress("Global", event, actions, false);
4057
4058 for (int i = 0; i < actions.size() && !handled; ++i)
4059 {
4060 const QString& action = actions[i];
4061 handled = true;
4062
4063 if (action == "0")
4064 {
4066 searchChanged();
4067 }
4068 else
4069 {
4070 handled = false;
4071 }
4072 }
4073
4074 if (!handled && MythScreenType::keyPressEvent(event))
4075 handled = true;
4076
4077 return handled;
4078}
4079
4081{
4083
4084 if (m_searchState)
4085 m_searchState->DisplayState(found ? "found" : "notfound");
4086}
4087
4089{
4090 bool found = m_parentList->FindNext();
4091
4092 if (m_searchState)
4093 m_searchState->DisplayState(found ? "found" : "notfound");
4094}
4095
4097{
4098 bool found = m_parentList->FindPrev();
4099
4100 if (m_searchState)
4101 m_searchState->DisplayState(found ? "found" : "notfound");
4102}
4103
4105{
4107 return;
4108
4109 int maximum = (m_itemCount <= m_itemsVisible) ? 0 : m_itemCount;
4110 m_scrollBar->SetMaximum(maximum);
4114}
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:98
bool Load(bool allowLoadInBackground=true, bool forceStat=false)
Load the image(s), wraps ImageLoader::LoadImage()
void SetFilename(const QString &filename)
Must be followed by a call to Load() to load the image.
void SetImage(MythImage *img)
Should not be used unless absolutely necessary since it bypasses the image caching and threaded loade...
void Reset(void) override
Reset the image back to the default defined in the theme.
Progress bar widget.
void Set(int start, int total, int used)
Scroll bar widget.
void SetMaximum(int value)
void SetPageStep(int value)
void SetSliderPosition(int value)
This widget is used for grouping other widgets for display when a particular named state is called.
MythUIType * GetState(const QString &name)
MythUIType * GetCurrentState()
void Reset(void) override
Reset the widget to it's original state, should not reset changes made by the theme.
bool DisplayState(const QString &name)
QString GetText(void) const
void SetText(const QString &text, bool moveCursor=true)
void valueChanged()
All purpose text widget, displays a text string.
Definition: mythuitext.h:29
void Reset(void) override
Reset the widget to it's original state, should not reset changes made by the theme.
Definition: mythuitext.cpp:65
QString GetTemplateText(void) const
Definition: mythuitext.h: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