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