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