openlp/openlp/core/ui/firsttimeform.py

568 lines
27 KiB
Python

# -*- coding: utf-8 -*-
##########################################################################
# OpenLP - Open Source Lyrics Projection #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2022 OpenLP Developers #
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
This module contains the first time wizard.
"""
import json
import logging
import time
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from tempfile import gettempdir
from PyQt5 import QtCore, QtWidgets, QtGui
from openlp.core.api.deploy import get_latest_size, download_and_check
from openlp.core.common import trace_error_handler
from openlp.core.common.applocation import AppLocation
from openlp.core.common.httputils import DownloadWorker, download_file, get_url_file_size, get_web_page
from openlp.core.common.i18n import translate
from openlp.core.common.mixins import RegistryProperties
from openlp.core.common.path import create_paths
from openlp.core.common.registry import Registry
from openlp.core.lib import build_icon
from openlp.core.lib.plugin import PluginStatus
from openlp.core.lib.ui import critical_error_message_box
from openlp.core.threading import get_thread_worker, is_thread_finished, run_thread
from openlp.core.ui.firsttimewizard import FirstTimePage, UiFirstTimeWizard
from openlp.core.ui.icons import UiIcons
from openlp.core.widgets.widgets import ProxyDialog
log = logging.getLogger(__name__)
class ThemeListWidgetItem(QtWidgets.QListWidgetItem):
"""
Subclass a QListWidgetItem to allow dynamic loading of thumbnails from an online resource
"""
def __init__(self, themes_url, sample_theme_data, ftw, *args, **kwargs):
super().__init__(*args, **kwargs)
title = sample_theme_data['title']
thumbnail = sample_theme_data['thumbnail']
self.file_name = sample_theme_data['file_name']
self.sha256 = sample_theme_data['sha256']
self.setIcon(UiIcons().picture) # Set a place holder icon whilst the thumbnails download
self.setText(title)
self.setToolTip(title)
worker = DownloadWorker(themes_url, thumbnail)
worker.download_failed.connect(self._on_download_failed)
worker.download_succeeded.connect(self._on_thumbnail_downloaded)
thread_name = 'thumbnail_download_{thumbnail}'.format(thumbnail=thumbnail)
run_thread(worker, thread_name)
ftw.thumbnail_download_threads.append(thread_name)
def _on_download_failed(self):
"""
Set an icon to indicate that the thumbnail download has failed.
:rtype: None
"""
self.setIcon(UiIcons().exception)
def _on_thumbnail_downloaded(self, thumbnail_path):
"""
Load the thumbnail as the icon when it has downloaded.
:param Path thumbnail_path: Path to the file to use as a thumbnail
:rtype: None
"""
self.setIcon(build_icon(thumbnail_path))
class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties):
"""
This is the FirstTimeWizard, designed to help new users to get up and running quickly.
"""
log.info('ThemeWizardForm loaded')
def __init__(self, parent=None):
"""
Create and set up the first time wizard.
"""
super(FirstTimeForm, self).__init__(parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowCloseButtonHint)
self.has_web_access = True
self.web = ''
self.is_index_downloaded = False
self.setup_ui(self)
self.customButtonClicked.connect(self._on_custom_button_clicked)
self.themes_list_widget.itemSelectionChanged.connect(self.on_themes_list_widget_selection_changed)
self.themes_deselect_all_button.clicked.connect(self.themes_list_widget.clearSelection)
self.themes_select_all_button.clicked.connect(self.themes_list_widget.selectAll)
self.setOption(QtWidgets.QWizard.HaveHelpButton, True)
self.helpRequested.connect(self.provide_help)
def provide_help(self):
"""
Provide help within the wizard by opening the appropriate page of the openlp manual in the user's browser
"""
QtGui.QDesktopServices.openUrl(QtCore.QUrl("https://manual.openlp.org/wizard.html"))
def get_next_page_id(self):
"""
Returns the id of the next FirstTimePage to go to based on enabled plugins
"""
if FirstTimePage.Remote < self.currentId() < FirstTimePage.Songs and self.songs_check_box.isChecked():
# If the songs plugin is enabled then go to the songs page
return FirstTimePage.Songs
elif FirstTimePage.Remote < self.currentId() < FirstTimePage.Bibles and self.bible_check_box.isChecked():
# Otherwise, if the Bibles plugin is enabled then go to the Bibles page
return FirstTimePage.Bibles
elif FirstTimePage.Remote < self.currentId() < FirstTimePage.Themes:
# Otherwise, if the current page is somewhere between the Welcome and the Themes pages, go to the themes
return FirstTimePage.Themes
else:
# If all else fails, go to the next page
return self.currentId() + 1
def nextId(self):
"""
Determine the next page in the Wizard to go to.
"""
self.application.process_events()
if self.currentId() == FirstTimePage.Download:
if not self.has_web_access:
return FirstTimePage.NoInternet
else:
return FirstTimePage.Remote
elif self.currentId() == FirstTimePage.Progress:
return -1
elif self.currentId() == FirstTimePage.NoInternet:
return FirstTimePage.Progress
return self.get_next_page_id()
def exec(self):
"""
Run the wizard.
"""
self.set_defaults()
return super().exec()
def initialize(self, screens):
"""
Set up the First Time Wizard
:param screens: The screens detected by OpenLP
"""
self.screens = screens
self.was_cancelled = False
self.thumbnail_download_threads = []
self.has_run_wizard = False
def _download_index(self):
"""
Download the configuration file and kick off the theme screenshot download threads
"""
# Check if the index file has already been downloaded
if self.is_index_downloaded:
return
# check to see if we have web access
self.has_web_access = False
self.config = ''
web_config = None
user_agent = 'OpenLP/' + QtWidgets.QApplication.applicationVersion()
self.application.process_events()
try:
web_config = get_web_page('{host}{name}'.format(host=self.web, name='download_3.0.json'),
headers={'User-Agent': user_agent})
except ConnectionError:
QtWidgets.QMessageBox.critical(self, translate('OpenLP.FirstTimeWizard', 'Network Error'),
translate('OpenLP.FirstTimeWizard', 'There was a network error attempting '
'to connect to retrieve initial configuration information'),
QtWidgets.QMessageBox.Ok)
if web_config and self._parse_config(web_config):
self.has_web_access = True
self.application.process_events()
self.downloading = translate('OpenLP.FirstTimeWizard', 'Downloading {name}...')
self.is_index_downloaded = True
def _parse_config(self, web_config):
try:
config = json.loads(web_config)
meta = config['_meta']
self.web = meta['base_url']
self.songs_url = self.web + meta['songs_dir'] + '/'
self.bibles_url = self.web + meta['bibles_dir'] + '/'
self.themes_url = self.web + meta['themes_dir'] + '/'
for song in config['songs'].values():
self.application.process_events()
item = QtWidgets.QListWidgetItem(song['title'], self.songs_list_widget)
item.setData(QtCore.Qt.UserRole, (song['file_name'], song['sha256']))
item.setCheckState(QtCore.Qt.Unchecked)
item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
for lang in config['bibles'].values():
self.application.process_events()
lang_item = QtWidgets.QTreeWidgetItem(self.bibles_tree_widget, [lang['title']])
for translation in lang['translations'].values():
self.application.process_events()
item = QtWidgets.QTreeWidgetItem(lang_item, [translation['title']])
item.setData(0, QtCore.Qt.UserRole, (translation['file_name'], translation['sha256']))
item.setCheckState(0, QtCore.Qt.Unchecked)
item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable)
self.bibles_tree_widget.expandAll()
self.application.process_events()
for theme in config['themes'].values():
ThemeListWidgetItem(self.themes_url, theme, self, self.themes_list_widget)
self.application.process_events()
except Exception:
log.exception('Unable to parse sample config file %s', web_config)
QtWidgets.QMessageBox.critical(self, translate('OpenLP.FirstTimeWizard', 'Invalid index file'),
translate('OpenLP.FirstTimeWizard',
'OpenLP was unable to read the resource index file. '
'Please try again later.'))
return False
return True
def set_defaults(self):
"""
Set up display at start of theme edit.
"""
self.restart()
self.web = 'https://get.openlp.org/ftw/'
self.currentIdChanged.connect(self.on_current_id_changed)
Registry().register_function('config_screen_changed', self.screen_selection_widget.load)
# Check if this is a re-run of the wizard.
self.has_run_wizard = self.settings.value('core/has run wizard')
create_paths(Path(gettempdir(), 'openlp'))
self.theme_combo_box.clear()
self.remote_page.can_download_remote = False
self.button(QtWidgets.QWizard.CustomButton1).setVisible(False)
if self.has_run_wizard:
self.songs_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songs').is_active())
self.bible_check_box.setChecked(self.plugin_manager.get_plugin_by_name('bibles').is_active())
self.presentation_check_box.setChecked(
self.plugin_manager.get_plugin_by_name('presentations').is_active())
self.image_check_box.setChecked(self.plugin_manager.get_plugin_by_name('images').is_active())
# temp fix for #677 when we have an error
try:
self.media_check_box.setChecked(self.plugin_manager.get_plugin_by_name('media').is_active())
except Exception:
self.media_check_box.setEnabled(False)
self.custom_check_box.setChecked(self.plugin_manager.get_plugin_by_name('custom').is_active())
self.song_usage_check_box.setChecked(self.plugin_manager.get_plugin_by_name('songusage').is_active())
self.alert_check_box.setChecked(self.plugin_manager.get_plugin_by_name('alerts').is_active())
# Add any existing themes to list.
self.theme_combo_box.insertSeparator(0)
self.theme_combo_box.addItems(sorted(self.theme_manager.get_theme_names()))
default_theme = self.settings.value('themes/global theme')
# Pre-select the current default theme.
index = self.theme_combo_box.findText(default_theme)
self.theme_combo_box.setCurrentIndex(index)
def on_current_id_changed(self, page_id):
"""
Detects Page changes and updates as appropriate.
"""
back_button = self.button(QtWidgets.QWizard.BackButton)
cancel_button = self.button(QtWidgets.QWizard.CancelButton)
internet_settings_button = self.button(QtWidgets.QWizard.CustomButton1)
next_button = self.button(QtWidgets.QWizard.NextButton)
back_button.setVisible(True)
next_button.setVisible(True)
internet_settings_button.setVisible(False)
self.application.process_events()
if page_id == FirstTimePage.SampleOption:
internet_settings_button.setVisible(True)
elif page_id == FirstTimePage.Download:
back_button.setVisible(False)
next_button.setVisible(False)
self.application.set_busy_cursor()
self._download_index()
self.application.set_normal_cursor()
self.next()
elif page_id == FirstTimePage.NoInternet:
next_button.setVisible(False)
cancel_button.setVisible(False)
internet_settings_button.setVisible(True)
elif page_id == FirstTimePage.Progress:
back_button.setVisible(False)
next_button.setVisible(False)
self.application.set_busy_cursor()
self._pre_wizard()
self._perform_wizard()
self._post_wizard()
self.application.set_normal_cursor()
def accept(self):
"""
Called when the user clicks 'Finish'. Reimplement it to to save the plugin status
:rtype: None
"""
self._set_plugin_status(self.songs_check_box, 'songs/status')
self._set_plugin_status(self.bible_check_box, 'bibles/status')
self._set_plugin_status(self.presentation_check_box, 'presentations/status')
self._set_plugin_status(self.image_check_box, 'images/status')
self._set_plugin_status(self.media_check_box, 'media/status')
self._set_plugin_status(self.custom_check_box, 'custom/status')
self._set_plugin_status(self.song_usage_check_box, 'songusage/status')
self._set_plugin_status(self.alert_check_box, 'alerts/status')
self.screen_selection_widget.save()
if self.theme_combo_box.currentIndex() != -1:
self.settings.setValue('themes/global theme', self.theme_combo_box.currentText())
Registry().remove_function('config_screen_changed', self.screen_selection_widget.load)
super().accept()
def reject(self):
"""
Called when the user clicks the cancel button. Reimplement it to clean up the threads.
:rtype: None
"""
self.was_cancelled = True
for thread_name in self.thumbnail_download_threads:
worker = get_thread_worker(thread_name)
if worker:
worker.cancel_download()
# Was the thread created.
if self.thumbnail_download_threads:
while any([not is_thread_finished(thread_name) for thread_name in self.thumbnail_download_threads]):
self.application.process_events()
time.sleep(0.1)
self.application.set_normal_cursor()
Registry().remove_function('config_screen_changed', self.screen_selection_widget.load)
super().reject()
def _on_custom_button_clicked(self, which):
"""
Slot to handle the a click on one of the wizards custom buttons.
:param int QtWidgets.QWizard which: The button pressed
:rtype: None
"""
# Internet settings button
if which == QtWidgets.QWizard.CustomButton1:
proxy_dialog = ProxyDialog(self)
proxy_dialog.retranslate_ui()
proxy_dialog.exec()
def on_projectors_check_box_clicked(self):
# When clicking projectors_check box, change the visibility setting for Projectors panel.
if self.settings.value('projector/show after wizard'):
self.settings.setValue('projector/show after wizard', False)
else:
self.settings.setValue('projector/show after wizard', True)
def on_themes_list_widget_selection_changed(self):
"""
Update the `theme_combo_box` with the selected items
:rtype: None
"""
existing_themes = []
if self.theme_manager:
existing_themes = self.theme_manager.get_theme_names()
for list_index in range(self.themes_list_widget.count()):
item = self.themes_list_widget.item(list_index)
if item.text() not in existing_themes:
cbox_index = self.theme_combo_box.findText(item.text())
if item.isSelected() and cbox_index == -1:
self.theme_combo_box.insertItem(0, item.text())
elif not item.isSelected() and cbox_index != -1:
self.theme_combo_box.removeItem(cbox_index)
def update_progress(self, count, block_size):
"""
Calculate and display the download progress. This method is called by download_file().
"""
increment = (count * block_size) - self.previous_size
self._increment_progress_bar(None, increment)
self.previous_size = count * block_size
def _increment_progress_bar(self, status_text, increment=1):
"""
Update the wizard progress page.
:param status_text: Current status information to display.
:param increment: The value to increment the progress bar by.
"""
if status_text:
self.progress_label.setText(status_text)
if increment > 0:
self.progress_bar.setValue(self.progress_bar.value() + increment)
self.application.process_events()
def _pre_wizard(self):
"""
Prepare the UI for the process.
"""
self.max_progress = 0
self.button(QtWidgets.QWizard.FinishButton).setEnabled(False)
self.application.process_events()
try:
# Loop through the songs list and increase for each selected item
for i in range(self.songs_list_widget.count()):
self.application.process_events()
item = self.songs_list_widget.item(i)
if item.checkState() == QtCore.Qt.Checked:
filename, sha256 = item.data(QtCore.Qt.UserRole)
size = get_url_file_size('{path}{name}'.format(path=self.songs_url, name=filename))
self.max_progress += size
# Loop through the Bibles list and increase for each selected item
iterator = QtWidgets.QTreeWidgetItemIterator(self.bibles_tree_widget)
while iterator.value():
self.application.process_events()
item = iterator.value()
if item.parent() and item.checkState(0) == QtCore.Qt.Checked:
filename, sha256 = item.data(0, QtCore.Qt.UserRole)
size = get_url_file_size('{path}{name}'.format(path=self.bibles_url, name=filename))
self.max_progress += size
iterator += 1
# Loop through the themes list and increase for each selected item
for item in self.themes_list_widget.selectedItems():
size = get_url_file_size('{url}{file}'.format(url=self.themes_url, file=item.file_name))
self.max_progress += size
# If we're downloading the remote, add it in here too
if self.remote_page.can_download_remote:
self.max_progress += get_latest_size()
except urllib.error.URLError:
trace_error_handler(log)
critical_error_message_box(translate('OpenLP.FirstTimeWizard', 'Download Error'),
translate('OpenLP.FirstTimeWizard', 'There was a connection problem during '
'download, so further downloads will be skipped. Try to re-run the '
'First Time Wizard later.'))
self.max_progress = 0
self.has_web_access = None
if self.max_progress:
# Add on 2 for plugins status setting plus a "finished" point.
self.max_progress += 2
self.progress_bar.setValue(0)
self.progress_bar.setMinimum(0)
self.progress_bar.setMaximum(self.max_progress)
self.progress_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Setting Up And Downloading'))
self.progress_page.setSubTitle(
translate('OpenLP.FirstTimeWizard', 'Please wait while OpenLP is set up and your data is downloaded.'))
else:
self.progress_bar.setVisible(False)
self.progress_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Setting Up'))
self.progress_page.setSubTitle('Setup complete.')
self.repaint()
self.application.process_events()
# Try to give the wizard a chance to repaint itself
time.sleep(0.1)
def _post_wizard(self):
"""
Clean up the UI after the process has finished.
"""
if self.max_progress:
self.progress_bar.setValue(self.progress_bar.maximum())
if self.has_run_wizard:
text = translate('OpenLP.FirstTimeWizard',
'Download complete. Click the \'{finish_button}\' button to return to OpenLP.')
else:
text = translate('OpenLP.FirstTimeWizard',
'Download complete. Click the \'{finish_button}\' button to start OpenLP.')
else:
if self.has_run_wizard:
text = translate('OpenLP.FirstTimeWizard', 'Click the \'{finish_button}\' button to return to OpenLP.')
else:
text = translate('OpenLP.FirstTimeWizard', 'Click the \'{finish_button}\' button to start OpenLP.')
self.progress_label.setText(text.format(finish_button=self.finish_button_text))
self.button(QtWidgets.QWizard.FinishButton).setEnabled(True)
self.button(QtWidgets.QWizard.CancelButton).setVisible(False)
self.application.process_events()
def _perform_wizard(self):
"""
Run the tasks in the wizard.
"""
if self.has_web_access:
if not self._download_selected():
critical_error_message_box(translate('OpenLP.FirstTimeWizard', 'Download Error'),
translate('OpenLP.FirstTimeWizard', 'There was a connection problem while '
'downloading, so further downloads will be skipped. Try to re-run '
'the First Time Wizard later.'))
def _download_selected(self):
"""
Download selected songs, bibles and themes. Returns False on download error
"""
# Build directories for downloads
songs_destination_path = Path(gettempdir(), 'openlp')
bibles_destination_path = AppLocation.get_section_data_path('bibles')
themes_destination_path = AppLocation.get_section_data_path('themes')
missed_files = []
# Download songs
for i in range(self.songs_list_widget.count()):
item = self.songs_list_widget.item(i)
if item.checkState() == QtCore.Qt.Checked:
filename, sha256 = item.data(QtCore.Qt.UserRole)
self._increment_progress_bar(self.downloading.format(name=filename), 0)
self.previous_size = 0
destination = songs_destination_path / str(filename)
if not download_file(self, '{path}{name}'.format(path=self.songs_url, name=filename),
destination, sha256):
missed_files.append('Song: {name}'.format(name=filename))
# Download Bibles
bibles_iterator = QtWidgets.QTreeWidgetItemIterator(self.bibles_tree_widget)
while bibles_iterator.value():
item = bibles_iterator.value()
if item.parent() and item.checkState(0) == QtCore.Qt.Checked:
bible, sha256 = item.data(0, QtCore.Qt.UserRole)
self._increment_progress_bar(self.downloading.format(name=bible), 0)
self.previous_size = 0
if not download_file(self, '{path}{name}'.format(path=self.bibles_url, name=bible),
bibles_destination_path / bible, sha256):
missed_files.append('Bible: {name}'.format(name=bible))
bibles_iterator += 1
# Download themes
for item in self.themes_list_widget.selectedItems():
self._increment_progress_bar(self.downloading.format(name=item.file_name), 0)
self.previous_size = 0
if not download_file(self, '{url}{file}'.format(url=self.themes_url, file=item.file_name),
themes_destination_path / item.file_name, item.sha256):
missed_files.append('Theme: {name}'.format(name=item.file_name))
# Remote
if self.remote_page.can_download_remote:
self._increment_progress_bar(self.downloading.format(name='Web Remote'), 0)
self.previous_size = 0
remote_version = download_and_check(self, can_update_range=False)
if remote_version:
self.settings.setValue('api/download version', remote_version)
else:
missed_files.append('Web Remote')
if missed_files:
file_list = ''
for entry in missed_files:
file_list += '{text}<br \\>'.format(text=entry)
msg = QtWidgets.QMessageBox()
msg.setIcon(QtWidgets.QMessageBox.Warning)
msg.setWindowTitle(translate('OpenLP.FirstTimeWizard', 'Network Error'))
msg.setText(translate('OpenLP.FirstTimeWizard', 'Unable to download some files'))
msg.setInformativeText(translate('OpenLP.FirstTimeWizard',
'The following files were not able to be '
'downloaded:<br \\>{text}'.format(text=file_list)))
msg.setStandardButtons(msg.Ok)
msg.exec()
return True
def _set_plugin_status(self, field, tag):
"""
Set the status of a plugin.
"""
status = PluginStatus.Active if field.checkState() == QtCore.Qt.Checked else PluginStatus.Inactive
self.settings.setValue(tag, status)