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