MythTV  master
themechooser.cpp
Go to the documentation of this file.
1 
2 // Theme Chooser headers
3 #include "themechooser.h"
4 
5 // Qt headers
6 #include <QCoreApplication>
7 #include <QRunnable>
8 #include <QRegExp>
9 
10 // MythTV headers
11 #include "mythcorecontext.h"
12 #include "mythcoreutil.h"
13 #include "mthreadpool.h"
14 #include "remotefile.h"
15 #include "mythdownloadmanager.h"
16 #include "programtypes.h"
17 #include "mythsystemevent.h"
18 #include "mythdate.h"
19 #include "mythversion.h"
20 #include "mythlogging.h"
21 #include "storagegroup.h"
22 
23 // LibMythUI headers
24 #include "mythmainwindow.h"
25 #include "mythuihelper.h"
26 #include "mythuiprogressbar.h"
27 #include "mythdialogbox.h"
28 #include "mythuibuttonlist.h"
29 #include "mythscreenstack.h"
30 #include "mythuistatetype.h"
31 #include "mythuigroup.h"
32 #include "mythuiimage.h"
33 #include "mythuitext.h"
34 
35 #define LOC QString("ThemeChooser: ")
36 #define LOC_WARN QString("ThemeChooser, Warning: ")
37 #define LOC_ERR QString("ThemeChooser, Error: ")
38 
42 class ThemeExtractThread : public QRunnable
43 {
44  public:
46  const QString &srcFile, const QString &destDir) :
47  m_parent(parent),
48  m_srcFile(srcFile),
49  m_destDir(destDir) {}
50 
51  void run() override // QRunnable
52  {
54 
55  MythEvent *me =
56  new MythEvent("THEME_INSTALLED", QStringList(m_srcFile));
57  QCoreApplication::postEvent(m_parent, me);
58  }
59 
60  private:
62  QString m_srcFile;
63  QString m_destDir;
64 };
65 
66 
72  const QString &name) :
73  MythScreenType(parent, name),
74  m_themes(nullptr),
75  m_preview(nullptr),
76  m_fullPreviewShowing(false),
77  m_fullPreviewStateType(nullptr),
78  m_fullScreenName(nullptr),
79  m_fullScreenPreview(nullptr),
80  m_refreshDownloadableThemes(false),
81  m_downloadTheme(nullptr),
82  m_downloadState(dsIdle),
83  m_popupMenu(nullptr)
84 {
86 
87  StorageGroup sgroup("Themes", gCoreContext->GetHostName());
88  m_userThemeDir = sgroup.GetFirstDir(true);
89 }
90 
92 {
94 }
95 
96 static bool sortThemeNames(const QFileInfo &s1, const QFileInfo &s2)
97 {
98  return s1.fileName().toLower() < s2.fileName().toLower();
99 }
100 
101 
103 {
104  // Load the theme for this screen
105  if (!LoadWindowFromXML("settings-ui.xml", "themechooser", this))
106  return false;
107 
108  bool err = false;
109  UIUtilE::Assign(this, m_themes, "themes", &err);
110 
111  UIUtilW::Assign(this, m_preview, "preview");
112  UIUtilW::Assign(this, m_fullPreviewStateType, "fullpreviewstate");
113 
115  {
116  MythUIGroup *state =
117  dynamic_cast<MythUIGroup*>
118  (m_fullPreviewStateType->GetChild("fullscreen"));
119  if (state)
120  {
122  dynamic_cast<MythUIText*>(state->GetChild("fullscreenname"));
124  dynamic_cast<MythUIImage*>(state->GetChild("fullscreenpreview"));
125  }
126  }
127 
128  if (err)
129  {
130  LOG(VB_GENERAL, LOG_ERR, LOC + "Cannot load screen 'themechooser'");
131  return false;
132  }
133 
134  connect(m_themes, SIGNAL(itemClicked(MythUIButtonListItem*)),
135  this, SLOT(saveAndReload(MythUIButtonListItem*)));
136  connect(m_themes, SIGNAL(itemSelected(MythUIButtonListItem*)),
137  this, SLOT(itemChanged(MythUIButtonListItem*)));
138 
139  BuildFocusList();
140 
142 
143  return true;
144 }
145 
147 {
148  SetBusyPopupMessage(tr("Loading Installed Themes"));
149 
150  QStringList themesSeen;
151  QDir themes(m_userThemeDir);
152  themes.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
153  themes.setSorting(QDir::Name | QDir::IgnoreCase);
154 
155  m_infoList = themes.entryInfoList();
156 
157  for( QFileInfoList::iterator it = m_infoList.begin();
158  it != m_infoList.end();
159  ++it )
160  {
161  if (loadThemeInfo(*it))
162  {
163  themesSeen << (*it).fileName();
164  m_themeStatuses[(*it).fileName()] = "default";
165  }
166  }
167 
168  themes.setPath(GetThemesParentDir());
169  QFileInfoList sharedThemes = themes.entryInfoList();
170  for( QFileInfoList::iterator it = sharedThemes.begin();
171  it != sharedThemes.end();
172  ++it )
173  {
174  if ((!themesSeen.contains((*it).fileName())) &&
175  (loadThemeInfo(*it)))
176  {
177  m_infoList << *it;
178  themesSeen << (*it).fileName();
179  m_themeStatuses[(*it).fileName()] = "default";
180  }
181  }
182 
183  // MYTH_SOURCE_VERSION - examples v29-pre-574-g92517f5, v29-Pre, v29.1-21-ge26a33c
184  QString MythVersion(MYTH_SOURCE_VERSION);
185  QRegExp trunkver("v[0-9]+-pre.*",Qt::CaseInsensitive);
186  QRegExp validver("v[0-9]+.*",Qt::CaseInsensitive);
187 
188  if (!validver.exactMatch(MythVersion))
189  {
190  LOG(VB_GENERAL, LOG_ERR, QString("Invalid MythTV version %1, will use themes from trunk").arg(MythVersion));
191  MythVersion = "trunk";
192  }
193  if (trunkver.exactMatch(MythVersion))
194  MythVersion = "trunk";
195 
196  if (MythVersion == "trunk")
197  {
198  LoadVersion(MythVersion, themesSeen, true);
199  LOG(VB_GUI, LOG_INFO, QString("Loading themes for %1").arg(MythVersion));
200  }
201  else
202  {
203 
204  MythVersion = MYTH_BINARY_VERSION; // Example: 29.20161017-1
205  // Remove the date part and the rest, eg 29.20161017-1 -> 29
206  MythVersion.replace(QRegExp("\\.[0-9]{8,}.*"), "");
207  LOG(VB_GUI, LOG_INFO, QString("Loading themes for %1").arg(MythVersion));
208  LoadVersion(MythVersion, themesSeen, true);
209 
210  // If a version of the theme for this tag exists, use it...
211  // MYTH_SOURCE_VERSION - examples v29-pre-574-g92517f5, v29-Pre, v29.1-21-ge26a33c
212  QRegExp subexp("v[0-9]+\\.([0-9]+)-*");
213  // This captures the subversion, i.e. the number after a dot
214  int pos = subexp.indexIn(MYTH_SOURCE_VERSION);
215  if (pos > -1)
216  {
217  QString subversion;
218  int idx = subexp.cap(1).toInt();
219  for ( ; idx > 0; --idx)
220  {
221  subversion = MythVersion + "." + QString::number(idx);
222  LOG(VB_GUI, LOG_INFO, QString("Loading themes for %1").arg(subversion));
223  LoadVersion(subversion, themesSeen, false);
224  }
225  }
226  }
227 
228  ResetBusyPopup();
229 
230  std::sort(m_infoList.begin(), m_infoList.end(), sortThemeNames);
231 }
232 
233 void ThemeChooser::LoadVersion(const QString &version,
234  QStringList &themesSeen, bool alert_user)
235 {
236  QString remoteThemesFile = GetConfDir();
237  remoteThemesFile.append("/tmp/themes.zip");
238  QString themeSite = QString("%1/%2")
239  .arg(gCoreContext->GetSetting("ThemeRepositoryURL",
240  "http://themes.mythtv.org/themes/repository")).arg(version);
241  QString destdir = GetCacheDir().append("/themechooser/");
242  QString versiondir = QString("%1/%2").arg(destdir).arg(version);
243  QDir remoteThemesDir(versiondir);
244 
245  int downloadFailures =
246  gCoreContext->GetNumSetting("ThemeInfoDownloadFailures", 0);
247  if (QFile::exists(remoteThemesFile))
248  {
249  QFileInfo finfo(remoteThemesFile);
250  if (finfo.lastModified().toUTC() <
251  MythDate::current().addSecs(-600))
252  {
253  LOG(VB_GUI, LOG_INFO, LOC +
254  QString("%1 is over 10 minutes old, forcing "
255  "remote theme list download").arg(remoteThemesFile));
257  }
258 
259  if (!remoteThemesDir.exists())
261  }
262  else if (downloadFailures < 2) // (and themes.zip does not exist)
263  {
264  LOG(VB_GUI, LOG_INFO, LOC +
265  QString("%1 does not exist, forcing remote theme "
266  "list download").arg(remoteThemesFile));
268  }
269 
271  {
272  QFile test(remoteThemesFile);
273  if (test.open(QIODevice::WriteOnly))
274  test.remove();
275  else
276  {
277  ShowOkPopup(tr("Unable to create '%1'").arg(remoteThemesFile));
278  return;
279  }
280 
281  SetBusyPopupMessage(tr("Refreshing Downloadable Themes Information"));
282 
283  QString url = themeSite;
284  url.append("/themes.zip");
285  if (!removeThemeDir(versiondir))
286  ShowOkPopup(tr("Unable to remove '%1'").arg(versiondir));
287  QDir dir;
288  if (!dir.mkpath(destdir))
289  ShowOkPopup(tr("Unable to create '%1'").arg(destdir));
290  bool result = GetMythDownloadManager()->download(url, remoteThemesFile, true);
291 
292  LOG(VB_GUI, LOG_INFO, LOC +
293  QString("Downloading '%1' to '%2'").arg(url).arg(remoteThemesFile));
294 
295  SetBusyPopupMessage(tr("Extracting Downloadable Themes Information"));
296 
297  if (!result || !extractZIP(remoteThemesFile, destdir))
298  {
299  QFile::remove(remoteThemesFile);
300 
301  downloadFailures++;
302  gCoreContext->SaveSetting("ThemeInfoDownloadFailures",
303  downloadFailures);
304 
305  if (!result)
306  {
307  LOG(VB_GUI, LOG_ERR, LOC +
308  QString("Failed to download '%1'").arg(url));
309  if (alert_user)
310  ShowOkPopup(tr("Failed to download '%1'").arg(url));
311  }
312  else
313  {
314  LOG(VB_GUI, LOG_ERR, LOC +
315  QString("Failed to unzip '%1' to '%2'")
316  .arg(remoteThemesFile).arg(destdir));
317  if (alert_user)
318  ShowOkPopup(tr("Failed to unzip '%1' to '%2'")
319  .arg(remoteThemesFile).arg(destdir));
320  }
321  }
322  else
323  LOG(VB_GUI, LOG_INFO, LOC +
324  QString("Unzipped '%1' to '%2'")
325  .arg(remoteThemesFile)
326  .arg(destdir));
327  }
328 
329  QDir themes;
330  themes.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
331  themes.setSorting(QDir::Name | QDir::IgnoreCase);
332 
333  if ((QFile::exists(remoteThemesFile)) &&
334  (remoteThemesDir.exists()))
335  {
336  SetBusyPopupMessage(tr("Loading Downloadable Themes"));
337 
338  LOG(VB_GUI, LOG_INFO, LOC +
339  QString("%1 and %2 exist, using cached remote themes list")
340  .arg(remoteThemesFile).arg(remoteThemesDir.absolutePath()));
341 
342  QString themesPath = remoteThemesDir.absolutePath();
343  themes.setPath(themesPath);
344 
345  QFileInfoList downloadableThemes = themes.entryInfoList();
346  for( QFileInfoList::iterator it = downloadableThemes.begin();
347  it != downloadableThemes.end();
348  ++it )
349  {
350  QString dirName = (*it).fileName();
351  QString themeName = dirName;
352  QString remoteDir = themeSite;
353  remoteDir.append("/").append(dirName);
354  QString localDir = themes.absolutePath();
355  localDir.append("/").append(dirName);
356 
357  ThemeInfo remoteTheme((*it).absoluteFilePath());
358 
359  if (themesSeen.contains(dirName))
360  {
361  ThemeInfo *localTheme = m_themeNameInfos[dirName];
362 
363  themeName = remoteTheme.GetName();
364 
365  int rmtMaj = remoteTheme.GetMajorVersion();
366  int rmtMin = remoteTheme.GetMinorVersion();
367  int locMaj = localTheme->GetMajorVersion();
368  int locMin = localTheme->GetMinorVersion();
369 
370  if ((rmtMaj > locMaj) ||
371  ((rmtMaj == locMaj) &&
372  (rmtMin > locMin)))
373  {
374  if (loadThemeInfo(*it))
375  {
376  LOG(VB_GUI, LOG_DEBUG, LOC +
377  QString("'%1' old version %2.%3, new version %4.%5")
378  .arg(themeName).arg(locMaj).arg(locMin)
379  .arg(rmtMaj).arg(rmtMin));
380 
381  m_infoList << *it;
382  m_themeStatuses[themeName] = "updateavailable";
383 
384  QFileInfo finfo(remoteTheme.GetPreviewPath());
386  remoteDir.append("/").append(finfo.fileName()),
387  localDir.append("/").append(finfo.fileName()),
388  nullptr);
389  }
390  }
391  else if ((rmtMaj == locMaj) &&
392  (rmtMin == locMin))
393  {
394  LOG(VB_GUI, LOG_DEBUG, LOC +
395  QString("'%1' up to date (%2.%3)")
396  .arg(themeName).arg(locMaj).arg(locMin));
397 
398  m_themeStatuses[themeName] = "uptodate";
399  }
400  }
401  else
402  {
403  LOG(VB_GUI, LOG_DEBUG, LOC +
404  QString("'%1' (%2.%3) available")
405  .arg(themeName)
406  .arg(remoteTheme.GetMajorVersion())
407  .arg(remoteTheme.GetMinorVersion()));
408 
409  ThemeInfo *tmpTheme = loadThemeInfo(*it);
410  if (tmpTheme)
411  {
412  themeName = tmpTheme->GetName();
413  themesSeen << dirName;
414  m_infoList << *it;
415  m_themeStatuses[themeName] = "updateavailable";
416 
417  QFileInfo finfo(tmpTheme->GetPreviewPath());
419  remoteDir.append("/").append(finfo.fileName()),
420  localDir.append("/").append(finfo.fileName()),
421  nullptr);
422  }
423  }
424  }
425  }
426 
427 }
428 
430 {
431  QString curTheme = gCoreContext->GetSetting("Theme");
432  ThemeInfo *themeinfo = nullptr;
433  ThemeInfo *curThemeInfo = nullptr;
434  MythUIButtonListItem *item = nullptr;
435 
436  m_themes->Reset();
437  for( QFileInfoList::iterator it = m_infoList.begin();
438  it != m_infoList.end();
439  ++it )
440  {
441  QFileInfo &theme = *it;
442 
443  if (!m_themeFileNameInfos.contains(theme.filePath()))
444  continue;
445 
446  themeinfo = m_themeFileNameInfos[theme.filePath()];
447  if (!themeinfo)
448  continue;
449 
450  QString buttonText = QString("%1 %2.%3")
451  .arg(themeinfo->GetName())
452  .arg(themeinfo->GetMajorVersion())
453  .arg(themeinfo->GetMinorVersion());
454 
455  item = new MythUIButtonListItem(m_themes, buttonText);
456  if (item)
457  {
458  if (themeinfo->GetDownloadURL().isEmpty())
459  item->DisplayState("local", "themelocation");
460  else
461  item->DisplayState("remote", "themelocation");
462 
463  item->DisplayState(themeinfo->GetAspect(), "aspectstate");
464 
465  item->DisplayState(m_themeStatuses[themeinfo->GetName()],
466  "themestatus");
467  InfoMap infomap;
468  themeinfo->ToMap(infomap);
469  item->SetTextFromMap(infomap);
470  item->SetData(qVariantFromValue(themeinfo));
471 
472  QString thumbnail = themeinfo->GetPreviewPath();
473  QFileInfo fInfo(thumbnail);
474  // Downloadable themeinfos have thumbnail copies of their preview images
475  if (!themeinfo->GetDownloadURL().isEmpty())
476  thumbnail = thumbnail.append(".thumb.jpg");
477  item->SetImage(thumbnail);
478 
479  if (curTheme == themeinfo->GetDirectoryName())
480  curThemeInfo = themeinfo;
481  }
482  }
483 
485 
486  if (curThemeInfo)
487  m_themes->SetValueByData(qVariantFromValue(curThemeInfo));
488 
490  if (current)
492 
493  QString testFile = m_userThemeDir + "/.test";
494  QFile test(testFile);
495  if (test.open(QIODevice::WriteOnly))
496  test.remove();
497  else
498  {
499  ShowOkPopup(tr("Error creating test file, %1 themes directory is "
500  "not writable.").arg(m_userThemeDir));
501  }
502 }
503 
505 {
506  if (theme.fileName() == "default" || theme.fileName() == "default-wide")
507  return nullptr;
508 
509  ThemeInfo *themeinfo = nullptr;
510  if (theme.exists()) // local directory vs http:// or remote URL
511  themeinfo = new ThemeInfo(theme.absoluteFilePath());
512  else
513  themeinfo = new ThemeInfo(theme.filePath());
514 
515  if (!themeinfo)
516  return nullptr;
517 
518  if (themeinfo->GetName().isEmpty() || !(themeinfo->GetType() & THEME_UI))
519  {
520  delete themeinfo;
521  return nullptr;
522  }
523 
524  m_themeFileNameInfos[theme.filePath()] = themeinfo;
525  m_themeNameInfos[theme.fileName()] = themeinfo;
526 
527  return themeinfo;
528 }
529 
531 {
532  if (m_popupMenu)
533  return;
534 
535  MythScreenStack *popupStack = GetMythMainWindow()->GetStack("popup stack");
536  QString label = tr("Theme Chooser Menu");
537 
538  m_popupMenu =
539  new MythDialogBox(label, popupStack, "themechoosermenupopup");
540 
541  connect(m_popupMenu, SIGNAL(Closed(QString, int)), SLOT(popupClosed(QString, int)));
542 
543  if (m_popupMenu->Create())
544  popupStack->AddScreen(m_popupMenu);
545  else
546  {
547  delete m_popupMenu;
548  m_popupMenu = nullptr;
549  return;
550  }
551 
552  m_popupMenu->SetReturnEvent(this, "popupmenu");
553 
555  {
557  m_popupMenu->AddButton(tr("Hide Fullscreen Preview"),
558  SLOT(toggleFullscreenPreview()));
559  else
560  m_popupMenu->AddButton(tr("Show Fullscreen Preview"),
561  SLOT(toggleFullscreenPreview()));
562  }
563 
564  m_popupMenu->AddButton(tr("Refresh Downloadable Themes"),
565  SLOT(refreshDownloadableThemes()));
566 
568  if (current)
569  {
570  ThemeInfo *info = current->GetData().value<ThemeInfo *>();
571 
572  if (info)
573  {
574  m_popupMenu->AddButton(tr("Select Theme"),
575  SLOT(saveAndReload()));
576 
577  if (info->GetPreviewPath().startsWith(m_userThemeDir))
578  m_popupMenu->AddButton(tr("Delete Theme"),
579  SLOT(removeTheme()));
580  }
581  }
582 
583  if (gCoreContext->GetBoolSetting("ThemeUpdateNofications", true))
584  m_popupMenu->AddButton(tr("Disable Theme Update Notifications"),
586  else
587  m_popupMenu->AddButton(tr("Enable Theme Update Notifications"),
589 }
590 
591 void ThemeChooser::popupClosed(QString which, int result)
592 {
593  (void)which;
594  (void)result;
595 
596  m_popupMenu = nullptr;
597 }
598 
599 bool ThemeChooser::keyPressEvent(QKeyEvent *event)
600 {
601  if (GetFocusWidget()->keyPressEvent(event))
602  return true;
603 
604  QStringList actions;
605  bool handled = GetMythMainWindow()->TranslateKeyPress("Theme Chooser", event, actions);
606 
607  for (int i = 0; i < actions.size() && !handled; ++i)
608  {
609  QString action = actions[i];
610  handled = true;
611 
612  if (action == "MENU")
613  showPopupMenu();
614  else if (action == "DELETE")
615  removeTheme();
616  else if ((action == "ESCAPE") &&
618  {
620  }
621  else
622  handled = false;
623  }
624 
625  if (!handled && MythScreenType::keyPressEvent(event))
626  handled = true;
627 
628  return handled;
629 }
630 
632 {
634  {
636  {
639 
640  if (m_fullScreenName)
642 
644  m_fullPreviewShowing = false;
645  }
646  else
647  {
649  ThemeInfo *info = item->GetData().value<ThemeInfo*>();
650  if (info)
651  {
653  {
656  }
657 
658  if (m_fullScreenName)
659  m_fullScreenName->SetText(info->GetName());
660 
661  m_fullPreviewStateType->DisplayState("fullscreen");
662  m_fullPreviewShowing = true;
663  }
664  }
665  }
666 }
667 
669 {
670  if (gCoreContext->GetBoolSetting("ThemeUpdateNofications", true))
671  gCoreContext->SaveSettingOnHost("ThemeUpdateNofications", "0", "");
672  else
673  gCoreContext->SaveSettingOnHost("ThemeUpdateNofications", "1", "");
674 }
675 
677 {
678  LOG(VB_GUI, LOG_INFO, LOC + "Forcing remote theme list refresh");
680  gCoreContext->SaveSetting("ThemeInfoDownloadFailures", 0);
682 }
683 
685 {
687  if (current)
689 }
690 
692 {
693  ThemeInfo *info = item->GetData().value<ThemeInfo *>();
694 
695  if (!info)
696  return;
697 
698  if (!info->GetDownloadURL().isEmpty())
699  {
700  QString testFile = m_userThemeDir + "/.test";
701  QFile test(testFile);
702  if (test.open(QIODevice::WriteOnly))
703  test.remove();
704  else
705  {
706  ShowOkPopup(tr("Unable to install theme, %1 themes directory is "
707  "not writable.").arg(m_userThemeDir));
708  return;
709  }
710 
711  QString downloadURL = info->GetDownloadURL();
712  QFileInfo qfile(downloadURL);
713  QString baseName = qfile.fileName();
714 
715  if (!gCoreContext->GetSetting("ThemeDownloadURL").isEmpty())
716  {
717  QStringList tokens =
718  gCoreContext->GetSetting("ThemeDownloadURL")
719  .split(";", QString::SkipEmptyParts);
720  QString origURL = downloadURL;
721  downloadURL.replace(tokens[0], tokens[1]);
722  LOG(VB_FILE, LOG_WARNING, LOC +
723  QString("Theme download URL overridden from %1 to %2.")
724  .arg(origURL).arg(downloadURL));
725  }
726 
727  OpenBusyPopup(tr("Downloading %1 Theme").arg(info->GetName()));
728  m_downloadTheme = info;
729 #if 0
731  "Temp", baseName);
733 #else
734  QString localFile = GetConfDir() + "/tmp/" + baseName;
735  GetMythDownloadManager()->queueDownload(downloadURL, localFile, this);
736  m_downloadFile = localFile;
738 #endif
739  }
740  else
741  {
742  gCoreContext->SaveSetting("Theme", info->GetDirectoryName());
743  GetMythMainWindow()->JumpTo("Reload Theme");
744  }
745 }
746 
748 {
749  ThemeInfo *info = item->GetData().value<ThemeInfo*>();
750 
751  if (!info)
752  return;
753 
754  QFileInfo preview(info->GetPreviewPath());
755  InfoMap infomap;
756  info->ToMap(infomap);
757  SetTextFromMap(infomap);
758  if (m_preview)
759  {
760  if (preview.exists())
761  {
763  m_preview->Load();
764  }
765  else
766  m_preview->Reset();
767  }
769  {
771  {
772  if (preview.exists())
773  {
776  }
777  else
779  }
780 
781  if (m_fullScreenName)
782  m_fullScreenName->SetText(info->GetName());
783  }
784 
785  MythUIStateType *themeLocation =
786  dynamic_cast<MythUIStateType*>(GetChild("themelocation"));
787  if (themeLocation)
788  {
789  if (info->GetDownloadURL().isEmpty())
790  themeLocation->DisplayState("local");
791  else
792  themeLocation->DisplayState("remote");
793  }
794 
795  MythUIStateType *aspectState =
796  dynamic_cast<MythUIStateType*>(GetChild("aspectstate"));
797  if (aspectState)
798  aspectState->DisplayState(info->GetAspect());
799 }
800 
801 void ThemeChooser::updateProgressBar(int bytesReceived,
802  int bytesTotal)
803 {
804  MythUIProgressBar *progressBar =
805  dynamic_cast<MythUIProgressBar *>(GetChild("downloadprogressbar"));
806 
807  if (!progressBar)
808  return;
809 
810  progressBar->SetUsed(bytesReceived);
811  progressBar->SetTotal(bytesTotal);
812 }
813 
815 {
816  if (e->type() == MythEvent::MythEventMessage)
817  {
818  MythEvent *me = static_cast<MythEvent *>(e);
819  QStringList tokens = me->Message().split(" ", QString::SkipEmptyParts);
820 
821  if (tokens.isEmpty())
822  return;
823 
824  if (tokens[0] == "DOWNLOAD_FILE")
825  {
826  QStringList args = me->ExtraDataList();
827  if ((m_downloadState == dsIdle) ||
828  (tokens.size() != 2) ||
829  (!m_downloadTheme) ||
830  (args[1] != m_downloadFile))
831  return;
832 
833  if (tokens[1] == "UPDATE")
834  {
835  updateProgressBar(args[2].toInt(), args[3].toInt());
836  }
837  else if (tokens[1] == "FINISHED")
838  {
839  bool remoteFileIsLocal = false;
840  int fileSize = args[2].toInt();
841  int errorCode = args[4].toInt();
842 
843  CloseBusyPopup();
844 
845  QFileInfo file(m_downloadFile);
847  (m_downloadFile.startsWith("myth://")))
848  {
849  // The backend download is finished so start the
850  // frontend download
851  if ((errorCode == 0) &&
852  (fileSize > 0))
853  {
855  QString localFile = GetConfDir() + "/tmp/" +
856  file.fileName();
857  file.setFile(localFile);
858 
859  if (file.exists())
860  {
861  remoteFileIsLocal = true;
862  m_downloadFile = localFile;
863  }
864  else
865  {
867  m_downloadFile, localFile, this);
868  OpenBusyPopup(tr("Copying %1 Theme Package")
869  .arg(m_downloadTheme->GetName()));
870  m_downloadFile = localFile;
871  return;
872  }
873  }
874  else
875  {
877  ShowOkPopup(tr("ERROR downloading theme package on master backend."));
878  }
879  }
880 
882  (file.exists()))
883  {
884  // The frontend download is finished
885  if ((errorCode == 0) &&
886  (fileSize > 0))
887  {
889  ThemeExtractThread *extractThread =
893  extractThread, "ThemeExtract");
894 
895  if (!remoteFileIsLocal)
897 
898  OpenBusyPopup(tr("Installing %1 Theme")
899  .arg(m_downloadTheme->GetName()));
900  }
901  else
902  {
904  ShowOkPopup(tr("ERROR downloading theme package from master backend."));
905  }
906  }
907  }
908  }
909  else if ((me->Message() == "THEME_INSTALLED") &&
910  (m_downloadTheme) &&
912  {
914  CloseBusyPopup();
915  QStringList args = me->ExtraDataList();
916  QFile::remove(args[0]);
917 
918  QString event = QString("THEME_INSTALLED PATH %1")
919  .arg(m_userThemeDir +
922 
924 
925  // Send a message to ourself so we trigger a reload our next chance
926  MythEvent *me2 = new MythEvent("THEME_RELOAD");
927  qApp->postEvent(this, me2);
928  }
929  else if ((me->Message() == "THEME_RELOAD") &&
930  (m_downloadState == dsIdle))
931  {
932  GetMythMainWindow()->JumpTo("Reload Theme");
933  }
934  }
935 }
936 
938 {
940  if (!current)
941  {
942  ShowOkPopup(tr("Error, no theme selected."));
943  return;
944  }
945 
946  ThemeInfo *info = current->GetData().value<ThemeInfo *>();
947  if (!info)
948  {
949  ShowOkPopup(tr("Error, unable to find current theme."));
950  return;
951  }
952 
953  if (!info->GetPreviewPath().startsWith(m_userThemeDir))
954  {
955  ShowOkPopup(tr("%1 is not a user-installed theme and can not "
956  "be deleted.").arg(info->GetName()));
957  return;
958  }
959 
961 
963 }
964 
965 bool ThemeChooser::removeThemeDir(const QString &dirname)
966 {
967  if ((!dirname.startsWith(m_userThemeDir)) &&
968  (!dirname.startsWith(GetMythUI()->GetThemeCacheDir())))
969  return true;
970 
971  QDir dir(dirname);
972 
973  if (!dir.exists())
974  return true;
975 
976  dir.setFilter(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
977  QFileInfoList list = dir.entryInfoList();
978  QFileInfoList::const_iterator it = list.begin();
979 
980  while (it != list.end())
981  {
982  const QFileInfo *fi = &(*it++);
983  if (fi->isFile() && !fi->isSymLink())
984  {
985  if (!QFile::remove(fi->absoluteFilePath()))
986  return false;
987  }
988  else if (fi->isDir() && !fi->isSymLink())
989  {
990  if (!removeThemeDir(fi->absoluteFilePath()))
991  return false;
992  }
993  }
994 
995  return dir.rmdir(dirname);
996 }
997 
999 
1001  m_updateTimer(new QTimer(this))
1002 {
1003  QString version = MYTH_SOURCE_PATH;
1004 
1005  if (!version.isEmpty() && !version.startsWith("fixes/"))
1006  // Treat devel branches as master
1007  m_mythVersions << "trunk";
1008  else
1009  {
1010  version = MYTH_BINARY_VERSION; // Example: 0.25.20101017-1
1011  version.replace(QRegExp("\\.[0-9]{8,}.*"), "");
1012 
1013  // If a version of the theme for this tag exists, use it...
1014  QRegExp subexp("v[0-9]+.[0-9]+.([0-9]+)-*");
1015  int pos = subexp.indexIn(MYTH_SOURCE_VERSION);
1016  if (pos > -1)
1017  {
1018  QString subversion;
1019  int idx = subexp.cap(1).toInt();
1020  for ( ; idx > 0; --idx)
1021  m_mythVersions << version + "." + QString::number(idx);
1022  }
1024  }
1025 
1028  "remotethemes/themes.zip",
1029  "Temp");
1030 
1031  gCoreContext->SaveSetting("ThemeUpdateStatus", "");
1032 
1033  connect(m_updateTimer, SIGNAL(timeout()), SLOT(checkForUpdate()));
1034 
1035  if (getenv("MYTHTV_DEBUGMDM"))
1036  {
1037  LOG(VB_GENERAL, LOG_INFO, "Checking for theme updates every minute");
1038  m_updateTimer->start(60 * 1000); // Run once a minute
1039  }
1040  else
1041  {
1042  LOG(VB_GENERAL, LOG_INFO, "Checking for theme updates every hour");
1043  m_updateTimer->start(60 * 60 * 1000); // Run once an hour
1044  }
1045 
1046  // Run once 15 seconds from now
1047  QTimer::singleShot(15 * 1000, this, SLOT(checkForUpdate()));
1048 }
1049 
1051 {
1052  if (m_updateTimer)
1053  {
1054  m_updateTimer->stop();
1055  delete m_updateTimer;
1056  m_updateTimer = nullptr;
1057  }
1058 }
1059 
1061 {
1062  if (GetMythUI()->GetCurrentLocation(false, true) != "mainmenu")
1063  return;
1064 
1065  ThemeInfo *localTheme = nullptr;
1066 
1068  {
1069  QStringList::iterator Iversion;
1070 
1071  for (Iversion = m_mythVersions.begin();
1072  Iversion != m_mythVersions.end(); ++Iversion)
1073  {
1074 
1075  QString remoteThemeDir =
1078  QString("remotethemes/%1/%2")
1079  .arg(*Iversion)
1080  .arg(GetMythUI()->GetThemeName()),
1081  "Temp");
1082 
1083  QString infoXML = remoteThemeDir;
1084  infoXML.append("/themeinfo.xml");
1085 
1086  LOG(VB_GUI, LOG_INFO, QString("ThemeUpdateChecker Loading '%1'")
1087  .arg(infoXML));
1088 
1089  if (RemoteFile::Exists(infoXML))
1090  {
1091  int locMaj = 0;
1092  int locMin = 0;
1093 
1094  ThemeInfo *remoteTheme = new ThemeInfo(remoteThemeDir);
1095  if (!remoteTheme || remoteTheme->GetType() & THEME_UNKN)
1096  {
1097  LOG(VB_GENERAL, LOG_ERR,
1098  QString("ThemeUpdateChecker::checkForUpdate(): "
1099  "Unable to create ThemeInfo for %1")
1100  .arg(infoXML));
1101  delete remoteTheme;
1102  remoteTheme = nullptr;
1103  return;
1104  }
1105 
1106  if (!localTheme)
1107  {
1108  localTheme = new ThemeInfo(GetMythUI()->GetThemeDir());
1109  if (!localTheme || localTheme->GetType() & THEME_UNKN)
1110  {
1111  LOG(VB_GENERAL, LOG_ERR,
1112  "ThemeUpdateChecker::checkForUpdate(): "
1113  "Unable to create ThemeInfo for current theme");
1114  delete localTheme;
1115  localTheme = nullptr;
1116  return;
1117  }
1118  locMaj = localTheme->GetMajorVersion();
1119  locMin = localTheme->GetMinorVersion();
1120  }
1121 
1122  int rmtMaj = remoteTheme->GetMajorVersion();
1123  int rmtMin = remoteTheme->GetMinorVersion();
1124 
1125  delete remoteTheme;
1126  remoteTheme = nullptr;
1127 
1128  if ((rmtMaj > locMaj) ||
1129  ((rmtMaj == locMaj) &&
1130  (rmtMin > locMin)))
1131  {
1133  QString("%1-%2.%3").arg(GetMythUI()->GetThemeName())
1134  .arg(rmtMaj).arg(rmtMin);
1135 
1136  QString status = gCoreContext->GetSetting
1137  ("ThemeUpdateStatus");
1138  QString currentLocation = GetMythUI()->GetCurrentLocation
1139  (false, true);
1140 
1141  if ((!status.startsWith(m_lastKnownThemeVersion)) &&
1142  (currentLocation == "mainmenu"))
1143  {
1144  m_currentVersion = QString("%1.%2")
1145  .arg(locMaj).arg(locMin);
1146  m_newVersion = QString("%1.%2").arg(rmtMaj).arg(rmtMin);
1147 
1148  gCoreContext->SaveSetting("ThemeUpdateStatus",
1150  + " notified");
1151 
1152  QString message = tr("Version %1 of the %2 theme is now "
1153  "available in the Theme Chooser. "
1154  "The currently installed version "
1155  "is %3.")
1156  .arg(m_newVersion)
1157  .arg(GetMythUI()->GetThemeName())
1158  .arg(m_currentVersion);
1159 
1160  ShowOkPopup(message);
1161  break;
1162  }
1163  }
1164  }
1165  }
1166  }
1167 
1168  delete localTheme;
1169 }
1170 
1171 /* vim: set expandtab tabstop=4 shiftwidth=4: */
This widget is used for grouping other widgets for display when a particular named state is called.
void ToMap(InfoMap &infoMap) const
Definition: themeinfo.cpp:254
static bool DeleteFile(const QString &url)
Definition: remotefile.cpp:425
bool removeThemeDir(const QString &dirname)
ThemeInfo * loadThemeInfo(QFileInfo &theme)
int GetMajorVersion() const
Definition: themeinfo.h:36
void updateProgressBar(int bytesReceived, int bytesTotal)
ThemeChooser(MythScreenStack *parent, const QString &name="ThemeChooser")
Creates a new ThemeChooser Screen.
QString GenMythURL(QString host=QString(), QString port=QString(), QString path=QString(), QString storageGroup=QString())
void start(QRunnable *runnable, QString debugName, int priority=0)
bool TranslateKeyPress(const QString &context, QKeyEvent *e, QStringList &actions, bool allowJumps=true)
Get a list of actions for a keypress in the given context.
MythConfirmationDialog * ShowOkPopup(const QString &message, QObject *parent, const char *slot, bool showCancel)
Non-blocking version of MythPopupBox::showOkPopup()
ThemeChooser * m_parent
void queueDownload(const QString &url, const QString &dest, QObject *caller, const bool reload=false)
Adds a url to the download queue.
void SetData(QVariant data)
QString GetAspect() const
Definition: themeinfo.h:28
void SaveSetting(const QString &key, int newValue)
static Type MythEventMessage
Definition: mythevent.h:65
void removeListener(QObject *listener)
Remove a listener to the observable.
int GetMasterServerPort(void)
Returns the Master Backend control port If no master server port has been defined in the database,...
Basic menu dialog, message and a list of options.
QString m_userThemeDir
Definition: themechooser.h:84
static bool downloadURL(const QString &url, QByteArray *buffer)
static bool sortThemeNames(const QFileInfo &s1, const QFileInfo &s2)
void removeTheme(void)
virtual void SetText(const QString &text)
Definition: mythuitext.cpp:168
MythScreenStack * GetStack(const QString &stackname)
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
QString m_lastKnownThemeVersion
Definition: themechooser.h:113
QString GetCurrentLocation(bool fullPath=false, bool mainStackOnly=true)
void addListener(QObject *listener)
Add a listener to the observable.
QMap< QString, ThemeInfo * > m_themeFileNameInfos
Definition: themechooser.h:87
DownloadState m_downloadState
Definition: themechooser.h:91
void itemChanged(MythUIButtonListItem *item)
bool extractZIP(const QString &zipFile, const QString &outDir)
bool Load(bool allowLoadInBackground=true, bool forceStat=false)
Load the image(s), wraps ImageLoader::LoadImage()
void BuildFocusList(void)
QString m_currentVersion
Definition: themechooser.h:114
bool m_fullPreviewShowing
Definition: themechooser.h:77
void SetImage(MythImage *image, const QString &name="")
Sets an image directly, should only be used in special circumstances since it bypasses the cache.
QString GetName() const
Definition: themeinfo.h:30
#define LOC
void checkForUpdate(void)
QString GetConfDir(void)
Definition: mythdirs.cpp:224
void showPopupMenu(void)
void AddButton(const QString &title, QVariant data=0, bool newMenu=false, bool setCurrent=false)
virtual void AddScreen(MythScreenType *screen, bool allowFade=true)
void SetValueByData(QVariant data)
This class is used as a container for messages.
Definition: mythevent.h:15
VERBOSE_PREAMBLE false
Definition: verbosedefs.h:85
void customEvent(QEvent *e) override
QHash< QString, QString > InfoMap
Definition: mythtypes.h:15
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:10
QString GetSetting(const QString &key, const QString &defaultval="")
void Init(void) override
Used after calling Load() to assign data to widgets and other UI initilisation which is prohibited in...
View and select installed themes.
Definition: themechooser.h:27
void Reset(void) override
Reset the widget to it's original state, should not reset changes made by the theme.
Definition: mythuitext.cpp:116
Create a group of widgets.
Definition: mythuigroup.h:11
MythDownloadManager * GetMythDownloadManager(void)
Gets the pointer to the MythDownloadManager singleton.
void run() override
MythUIImage * m_fullScreenPreview
Definition: themechooser.h:80
bool download(const QString &url, const QString &dest, const bool reload=false)
Downloads a URL to a file in blocking mode.
void ReloadInBackground(void)
void Reset(void) override
Reset the image back to the default defined in the theme.
void refreshDownloadableThemes(void)
virtual void SetTextFromMap(const InfoMap &infoMap)
QString GetMasterHostName(void)
static bool Assign(ContainerType *container, UIType *&item, const QString &name, bool *err=nullptr)
Definition: mythuiutils.h:27
int GetMinorVersion() const
Definition: themeinfo.h:37
bool m_refreshDownloadableThemes
Definition: themechooser.h:83
void LoadVersion(const QString &version, QStringList &themesSeen, bool alert_user)
void JumpTo(const QString &destination, bool pop=true)
void SetBusyPopupMessage(const QString &message)
MythDialogBox * m_popupMenu
Definition: themechooser.h:93
const char * name
Definition: ParseText.cpp:339
MythUIHelper * GetMythUI()
static bool LoadWindowFromXML(const QString &xmlfile, const QString &windowname, MythUIType *parent)
MythUIType * GetFocusWidget(void) const
void Reset() override
Reset the widget to it's original state, should not reset changes made by the theme.
MythMainWindow * GetMythMainWindow(void)
MythUIImage * m_preview
Definition: themechooser.h:75
QString RemoteDownloadFile(const QString &url, const QString &storageGroup, const QString &filename)
static MThreadPool * globalInstance(void)
static bool Exists(const QString &url, struct stat *fileinfo)
Definition: remotefile.cpp:468
int GetNumSetting(const QString &key, int defaultval=0)
void toggleFullscreenPreview(void)
#define MYTH_SOURCE_PATH
Definition: version.h:3
QMap< QString, ThemeInfo * > m_themeNameInfos
Definition: themechooser.h:86
QString GetCacheDir(void)
Returns the base directory for all cached files.
Definition: mythdirs.cpp:234
int GetType() const
Definition: themeinfo.h:35
bool keyPressEvent(QKeyEvent *) override
Key event handler.
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
void CloseBusyPopup(void)
QMap< QString, QString > m_themeStatuses
Definition: themechooser.h:88
bool GetBoolSetting(const QString &key, bool defaultval=false)
ThemeInfo * m_downloadTheme
Definition: themechooser.h:89
QString GetDownloadURL() const
Definition: themeinfo.h:39
void SetFilename(const QString &filename)
Must be followed by a call to Load() to load the image.
void SetReturnEvent(QObject *retobject, const QString &resultid)
QFileInfoList m_infoList
Definition: themechooser.h:82
QString m_downloadFile
Definition: themechooser.h:90
#define MYTH_BINARY_VERSION
Update this whenever the plug-in ABI changes.
Definition: mythversion.h:16
bool DisplayState(const QString &name)
bool Create(void) override
void Reset(void) override
Reset the widget to it's original state, should not reset changes made by the theme.
string themeName
Definition: mythburn.py:189
void toggleThemeUpdateNotifications(void)
void saveAndReload(void)
QString GetThemesParentDir(void)
Definition: mythdirs.cpp:225
bool SetFocusWidget(MythUIType *widget=nullptr)
QTimer * m_updateTimer
Definition: themechooser.h:110
void OpenBusyPopup(QString message="")
MythUIButtonList * m_themes
Definition: themechooser.h:74
void popupClosed(QString which, int result)
bool SaveSettingOnHost(const QString &key, const QString &newValue, const QString &host)
Screen in which all other widgets are contained and rendered.
QString GetDirectoryName() const
Definition: themeinfo.h:43
Progress bar widget.
QStringList m_mythVersions
Definition: themechooser.h:111
void Load(void) override
Load data which will ultimately be displayed on-screen or used to determine what appears on-screen (S...
const QString & Message() const
Definition: mythevent.h:57
ThemeExtractThread(ThemeChooser *parent, const QString &srcFile, const QString &destDir)
QString GetHostName(void)
bool keyPressEvent(QKeyEvent *) override
Key event handler.
MythUIType * GetChild(const QString &name) const
Get a named child of this UIType.
Definition: mythuitype.cpp:155
void DisplayState(const QString &state, const QString &name)
void SetTextFromMap(const InfoMap &infoMap, const QString &state="")
void SendSystemEvent(const QString &msg)
MythUIText * m_fullScreenName
Definition: themechooser.h:79
#define MYTH_SOURCE_VERSION
Definition: version.h:2
const QStringList & ExtraDataList() const
Definition: mythevent.h:59
void ResetBusyPopup(void)
QString GetPreviewPath() const
Definition: themeinfo.h:34
MythUIStateType * m_fullPreviewStateType
Definition: themechooser.h:78
MythUIButtonListItem * GetItemCurrent() const
void LoadInBackground(QString message="")
bool Create(void) override