diff --git a/openlp/core/common/httputils.py b/openlp/core/common/httputils.py index 393bab353..c9a555a55 100644 --- a/openlp/core/common/httputils.py +++ b/openlp/core/common/httputils.py @@ -27,12 +27,16 @@ import logging import sys import time from random import randint +from tempfile import gettempdir import requests +from PyQt5 import QtCore from openlp.core.common import trace_error_handler +from openlp.core.common.path import Path from openlp.core.common.registry import Registry from openlp.core.common.settings import ProxyMode, Settings +from openlp.core.threading import ThreadWorker log = logging.getLogger(__name__ + '.__init__') @@ -227,4 +231,46 @@ def download_file(update_object, url, file_path, sha256=None): return True -__all__ = ['get_web_page'] +class DownloadWorker(ThreadWorker): + """ + This worker allows a file to be downloaded in a thread + """ + download_failed = QtCore.pyqtSignal() + download_succeeded = QtCore.pyqtSignal(Path) + + def __init__(self, base_url, file_name): + """ + Set up the worker object + """ + self._base_url = base_url + self._file_name = file_name + self._download_cancelled = False + super().__init__() + + def start(self): + """ + Download the url to the temporary directory + """ + if self._download_cancelled: + self.quit.emit() + return + try: + dest_path = Path(gettempdir()) / 'openlp' / self._file_name + url = '{url}{name}'.format(url=self._base_url, name=self._file_name) + is_success = download_file(self, url, dest_path) + if is_success and not self._download_cancelled: + self.download_succeeded.emit(dest_path) + else: + self.download_failed.emit() + except Exception: + log.exception('Unable to download %s', url) + self.download_failed.emit() + finally: + self.quit.emit() + + @QtCore.pyqtSlot() + def cancel_download(self): + """ + A slot to allow the download to be cancelled from outside of the thread + """ + self._download_cancelled = True diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index 0aa753029..d5f48e2d7 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -22,19 +22,19 @@ """ This module contains the first time wizard. """ +import json import logging import time import urllib.error import urllib.parse import urllib.request -from configparser import ConfigParser, MissingSectionHeaderError, NoOptionError, NoSectionError from tempfile import gettempdir from PyQt5 import QtCore, QtWidgets from openlp.core.common import clean_button_text, trace_error_handler from openlp.core.common.applocation import AppLocation -from openlp.core.common.httputils import download_file, get_url_file_size, get_web_page +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 Path, create_paths @@ -43,57 +43,50 @@ from openlp.core.common.settings import Settings 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 ThreadWorker, get_thread_worker, is_thread_finished, run_thread +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 log = logging.getLogger(__name__) -class ThemeScreenshotWorker(ThreadWorker): +class ThemeListWidgetItem(QtWidgets.QListWidgetItem): """ - This thread downloads a theme's screenshot + Subclass a QListWidgetItem to allow dynamic loading of thumbnails from an online resource """ - screenshot_downloaded = QtCore.pyqtSignal(str, str, str) + 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 __init__(self, themes_url, title, filename, sha256, screenshot): + def _on_download_failed(self): """ - Set up the worker object - """ - self.was_cancelled = False - self.themes_url = themes_url - self.title = title - self.filename = filename - self.sha256 = sha256 - self.screenshot = screenshot - super().__init__() + Set an icon to indicate that the thumbnail download has failed. - def start(self): + :rtype: None """ - Run the worker - """ - if self.was_cancelled: - return - try: - download_path = Path(gettempdir()) / 'openlp' / self.screenshot - is_success = download_file(self, '{host}{name}'.format(host=self.themes_url, name=self.screenshot), - download_path) - if is_success and not self.was_cancelled: - # Signal that the screenshot has been downloaded - self.screenshot_downloaded.emit(self.title, self.filename, self.sha256) - except: # noqa - log.exception('Unable to download screenshot') - finally: - self.quit.emit() + self.setIcon(UiIcons().exception) - @QtCore.pyqtSlot(bool) - def set_download_canceled(self, toggle): + def _on_thumbnail_downloaded(self, thumbnail_path): """ - Externally set if the download was canceled + Load the thumbnail as the icon when it has downloaded. - :param toggle: Set if the download was canceled or not + :param Path thumbnail_path: Path to the file to use as a thumbnail + :rtype: None """ - self.was_download_cancelled = toggle + self.setIcon(build_icon(thumbnail_path)) class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): @@ -110,6 +103,9 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): self.web_access = True self.web = '' self.setup_ui(self) + 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) def get_next_page_id(self): """ @@ -144,18 +140,7 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): return -1 elif self.currentId() == FirstTimePage.NoInternet: return FirstTimePage.Progress - elif self.currentId() == FirstTimePage.Themes: - self.application.set_busy_cursor() - while not all([is_thread_finished(thread_name) for thread_name in self.theme_screenshot_threads]): - time.sleep(0.1) - self.application.process_events() - # Build the screenshot icons, as this can not be done in the thread. - self._build_theme_screenshots() - self.application.set_normal_cursor() - self.theme_screenshot_threads = [] - return self.get_next_page_id() - else: - return self.get_next_page_id() + return self.get_next_page_id() def exec(self): """ @@ -172,104 +157,76 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): """ self.screens = screens self.was_cancelled = False - self.theme_screenshot_threads = [] + self.thumbnail_download_threads = [] self.has_run_wizard = False - self.themes_list_widget.itemChanged.connect(self.on_theme_selected) - def _download_index(self): """ Download the configuration file and kick off the theme screenshot download threads """ # check to see if we have web access self.web_access = False - self.config = ConfigParser() + self.config = '' + web_config = None user_agent = 'OpenLP/' + Registry().get('application').applicationVersion() self.application.process_events() try: - web_config = get_web_page('{host}{name}'.format(host=self.web, name='download.cfg'), + 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) - web_config = False - if web_config: - try: - self.config.read_string(web_config) - self.web = self.config.get('general', 'base url') - self.songs_url = self.web + self.config.get('songs', 'directory') + '/' - self.bibles_url = self.web + self.config.get('bibles', 'directory') + '/' - self.themes_url = self.web + self.config.get('themes', 'directory') + '/' - self.web_access = True - except (NoSectionError, NoOptionError, MissingSectionHeaderError): - log.debug('A problem occurred while parsing the downloaded config file') - trace_error_handler(log) + if web_config and self._parse_config(web_config): + self.web_access = True self.application.process_events() self.downloading = translate('OpenLP.FirstTimeWizard', 'Downloading {name}...') - 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()) - self.media_check_box.setChecked(self.plugin_manager.get_plugin_by_name('media').is_active()) - 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()) self.application.set_normal_cursor() - # Sort out internet access for downloads - if self.web_access: - songs = self.config.get('songs', 'languages') - songs = songs.split(',') - for song in songs: + + 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() - title = self.config.get('songs_{song}'.format(song=song), 'title') - filename = self.config.get('songs_{song}'.format(song=song), 'filename') - sha256 = self.config.get('songs_{song}'.format(song=song), 'sha256', fallback='') - item = QtWidgets.QListWidgetItem(title, self.songs_list_widget) - item.setData(QtCore.Qt.UserRole, (filename, sha256)) + 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) - bible_languages = self.config.get('bibles', 'languages') - bible_languages = bible_languages.split(',') - for lang in bible_languages: + for lang in config['bibles'].values(): self.application.process_events() - language = self.config.get('bibles_{lang}'.format(lang=lang), 'title') - lang_item = QtWidgets.QTreeWidgetItem(self.bibles_tree_widget, [language]) - bibles = self.config.get('bibles_{lang}'.format(lang=lang), 'translations') - bibles = bibles.split(',') - for bible in bibles: + lang_item = QtWidgets.QTreeWidgetItem(self.bibles_tree_widget, [lang['title']]) + for translation in lang['translations'].values(): self.application.process_events() - title = self.config.get('bible_{bible}'.format(bible=bible), 'title') - filename = self.config.get('bible_{bible}'.format(bible=bible), 'filename') - sha256 = self.config.get('bible_{bible}'.format(bible=bible), 'sha256', fallback='') - item = QtWidgets.QTreeWidgetItem(lang_item, [title]) - item.setData(0, QtCore.Qt.UserRole, (filename, sha256)) + 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() - # Download the theme screenshots - themes = self.config.get('themes', 'files').split(',') - for theme in themes: - title = self.config.get('theme_{theme}'.format(theme=theme), 'title') - filename = self.config.get('theme_{theme}'.format(theme=theme), 'filename') - sha256 = self.config.get('theme_{theme}'.format(theme=theme), 'sha256', fallback='') - screenshot = self.config.get('theme_{theme}'.format(theme=theme), 'screenshot') - worker = ThemeScreenshotWorker(self.themes_url, title, filename, sha256, screenshot) - worker.screenshot_downloaded.connect(self.on_screenshot_downloaded) - thread_name = 'theme_screenshot_{title}'.format(title=title) - run_thread(worker, thread_name) - self.theme_screenshot_threads.append(thread_name) + 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) + critical_error_message_box( + 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 = 'http://openlp.org/files/frw/' + self.web = 'https://get.openlp.org/ftw/' self.cancel_button.clicked.connect(self.on_cancel_button_clicked) self.no_internet_finish_button.clicked.connect(self.on_no_internet_finish_button_clicked) self.no_internet_cancel_button.clicked.connect(self.on_no_internet_cancel_button_clicked) @@ -282,9 +239,18 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): create_paths(Path(gettempdir(), 'openlp')) self.theme_combo_box.clear() 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()) + self.media_check_box.setChecked(self.plugin_manager.get_plugin_by_name('media').is_active()) + 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. - for theme in self.theme_manager.get_themes(): - self.theme_combo_box.addItem(theme) + self.theme_combo_box.insertSeparator(0) + self.theme_combo_box.addItems(sorted(self.theme_manager.get_themes())) default_theme = Settings().value('themes/global theme') # Pre-select the current default theme. index = self.theme_combo_box.findText(default_theme) @@ -335,49 +301,34 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): Process the triggering of the cancel button. """ self.was_cancelled = True - if self.theme_screenshot_threads: - for thread_name in self.theme_screenshot_threads: + if self.thumbnail_download_threads: # TODO: Use main thread list + for thread_name in self.thumbnail_download_threads: worker = get_thread_worker(thread_name) if worker: - worker.set_download_canceled(True) + worker.cancel_download() # Was the thread created. - if self.theme_screenshot_threads: - while any([not is_thread_finished(thread_name) for thread_name in self.theme_screenshot_threads]): + if self.thumbnail_download_threads: + while any([not is_thread_finished(thread_name) for thread_name in self.thumbnail_download_threads]): time.sleep(0.1) self.application.set_normal_cursor() - def on_screenshot_downloaded(self, title, filename, sha256): + def on_themes_list_widget_selection_changed(self): """ - Add an item to the list when a theme has been downloaded + Update the `theme_combo_box` with the selected items - :param title: The title of the theme - :param filename: The filename of the theme - """ - self.themes_list_widget.blockSignals(True) - item = QtWidgets.QListWidgetItem(title, self.themes_list_widget) - item.setData(QtCore.Qt.UserRole, (filename, sha256)) - item.setCheckState(QtCore.Qt.Unchecked) - item.setFlags(item.flags() | QtCore.Qt.ItemIsUserCheckable) - self.themes_list_widget.blockSignals(False) - - def on_theme_selected(self, item): - """ - Add or remove a de/selected sample theme from the theme_combo_box - - :param QtWidgets.QListWidgetItem item: The item that has been de/selected :rtype: None """ - theme_name = item.text() - if self.theme_manager and theme_name in self.theme_manager.get_themes(): - return True - if item.checkState() == QtCore.Qt.Checked: - self.theme_combo_box.addItem(theme_name) - return True - else: - index = self.theme_combo_box.findText(theme_name) - if index != -1: - self.theme_combo_box.removeItem(index) - return True + existing_themes = [] + if self.theme_manager: + existing_themes = self.theme_manager.get_themes() + 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 on_no_internet_finish_button_clicked(self): """ @@ -396,18 +347,6 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): self.was_cancelled = True self.close() - def _build_theme_screenshots(self): - """ - This method builds the theme screenshots' icons for all items in the ``self.themes_list_widget``. - """ - themes = self.config.get('themes', 'files') - themes = themes.split(',') - for index, theme in enumerate(themes): - screenshot = self.config.get('theme_{theme}'.format(theme=theme), 'screenshot') - item = self.themes_list_widget.item(index) - if item: - item.setIcon(build_icon(Path(gettempdir(), 'openlp', screenshot))) - def update_progress(self, count, block_size): """ Calculate and display the download progress. This method is called by download_file(). @@ -456,13 +395,9 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): self.max_progress += size iterator += 1 # Loop through the themes list and increase for each selected item - for i in range(self.themes_list_widget.count()): - self.application.process_events() - item = self.themes_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.themes_url, name=filename)) - self.max_progress += size + 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 except urllib.error.URLError: trace_error_handler(log) critical_error_message_box(translate('OpenLP.FirstTimeWizard', 'Download Error'), @@ -579,15 +514,12 @@ class FirstTimeForm(QtWidgets.QWizard, UiFirstTimeWizard, RegistryProperties): missed_files.append('Bible: {name}'.format(name=bible)) bibles_iterator += 1 # Download themes - for i in range(self.themes_list_widget.count()): - item = self.themes_list_widget.item(i) - if item.checkState() == QtCore.Qt.Checked: - theme, sha256 = item.data(QtCore.Qt.UserRole) - self._increment_progress_bar(self.downloading.format(name=theme), 0) - self.previous_size = 0 - if not download_file(self, '{path}{name}'.format(path=self.themes_url, name=theme), - themes_destination_path / theme, sha256): - missed_files.append('Theme: {name}'.format(name=theme)) + 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)) if missed_files: file_list = '' for entry in missed_files: diff --git a/openlp/core/ui/firsttimewizard.py b/openlp/core/ui/firsttimewizard.py index f2ac33604..37b9389a7 100644 --- a/openlp/core/ui/firsttimewizard.py +++ b/openlp/core/ui/firsttimewizard.py @@ -49,6 +49,39 @@ class FirstTimePage(object): Progress = 8 +class ThemeListWidget(QtWidgets.QListWidget): + """ + Subclass a QListWidget so we can make it look better when it resizes. + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) + self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) + self.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) + self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + self.setIconSize(QtCore.QSize(133, 100)) + self.setMovement(QtWidgets.QListView.Static) + self.setFlow(QtWidgets.QListView.LeftToRight) + self.setProperty("isWrapping", True) + self.setResizeMode(QtWidgets.QListView.Adjust) + self.setViewMode(QtWidgets.QListView.IconMode) + self.setUniformItemSizes(True) + + def resizeEvent(self, event): + """ + Resize the grid so the list looks better when its resized/ + + :param QtGui.QResizeEvent event: Not used + :return: None + """ + nominal_width = 141 # Icon width of 133 + 4 each side + max_items_per_row = self.viewport().width() // nominal_width or 1 # or 1 to avoid divide by 0 errors + col_size = (self.viewport().width() - 1) / max_items_per_row + self.setGridSize(QtCore.QSize(col_size, 140)) + + class UiFirstTimeWizard(object): """ The UI widgets for the first time wizard. @@ -175,27 +208,26 @@ class UiFirstTimeWizard(object): self.themes_page = QtWidgets.QWizardPage() self.themes_page.setObjectName('themes_page') self.themes_layout = QtWidgets.QVBoxLayout(self.themes_page) - self.themes_layout.setContentsMargins(20, 50, 20, 60) self.themes_layout.setObjectName('themes_layout') - self.themes_list_widget = QtWidgets.QListWidget(self.themes_page) - self.themes_list_widget.setViewMode(QtWidgets.QListView.IconMode) - self.themes_list_widget.setMovement(QtWidgets.QListView.Static) - self.themes_list_widget.setFlow(QtWidgets.QListView.LeftToRight) - self.themes_list_widget.setSpacing(4) - self.themes_list_widget.setUniformItemSizes(True) - self.themes_list_widget.setIconSize(QtCore.QSize(133, 100)) - self.themes_list_widget.setWrapping(False) - self.themes_list_widget.setObjectName('themes_list_widget') + self.themes_list_widget = ThemeListWidget(self.themes_page) self.themes_layout.addWidget(self.themes_list_widget) + self.theme_options_layout = QtWidgets.QHBoxLayout() self.default_theme_layout = QtWidgets.QHBoxLayout() self.theme_label = QtWidgets.QLabel(self.themes_page) self.default_theme_layout.addWidget(self.theme_label) self.theme_combo_box = QtWidgets.QComboBox(self.themes_page) self.theme_combo_box.setEditable(False) - self.theme_combo_box.setInsertPolicy(QtWidgets.QComboBox.NoInsert) - self.theme_combo_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) - self.default_theme_layout.addWidget(self.theme_combo_box) - self.themes_layout.addLayout(self.default_theme_layout) + self.default_theme_layout.addWidget(self.theme_combo_box, stretch=1) + self.theme_options_layout.addLayout(self.default_theme_layout, stretch=1) + self.select_buttons_layout = QtWidgets.QHBoxLayout() + self.themes_select_all_button = QtWidgets.QToolButton(self.themes_page) + self.themes_select_all_button.setIcon(UiIcons().select_all) + self.select_buttons_layout.addWidget(self.themes_select_all_button, stretch=1, alignment=QtCore.Qt.AlignRight) + self.themes_deselect_all_button = QtWidgets.QToolButton(self.themes_page) + self.themes_deselect_all_button.setIcon(UiIcons().select_none) + self.select_buttons_layout.addWidget(self.themes_deselect_all_button) + self.theme_options_layout.addLayout(self.select_buttons_layout, stretch=1) + self.themes_layout.addLayout(self.theme_options_layout) first_time_wizard.setPage(FirstTimePage.Themes, self.themes_page) # Progress page self.progress_page = QtWidgets.QWizardPage() @@ -271,9 +303,12 @@ class UiFirstTimeWizard(object): self.songs_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download public domain songs.')) self.bibles_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Bibles')) self.bibles_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download free Bibles.')) + # Themes Page self.themes_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Themes')) self.themes_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download sample themes.')) - self.theme_label.setText(translate('OpenLP.FirstTimeWizard', 'Select default theme:')) + self.theme_label.setText(translate('OpenLP.FirstTimeWizard', 'Default theme:')) + self.themes_select_all_button.setToolTip(translate('OpenLP.FirstTimeWizard', 'Select all')) + self.themes_deselect_all_button.setToolTip(translate('OpenLP.FirstTimeWizard', 'Deselect all')) self.progress_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Downloading and Configuring')) self.progress_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Please wait while resources are downloaded ' 'and OpenLP is configured.')) diff --git a/openlp/core/ui/icons.py b/openlp/core/ui/icons.py index d62e9fce5..5aa157e9f 100644 --- a/openlp/core/ui/icons.py +++ b/openlp/core/ui/icons.py @@ -138,6 +138,8 @@ class UiIcons(object): 'search_plus': {'icon': 'fa.search-plus'}, 'search_ref': {'icon': 'fa.institution'}, 'search_text': {'icon': 'op.search-text'}, + 'select_all': {'icon': 'fa.check-square-o'}, + 'select_none': {'icon': 'fa.square-o'}, 'settings': {'icon': 'fa.cogs'}, 'shortcuts': {'icon': 'fa.wrench'}, 'song_usage': {'icon': 'fa.line-chart'}, diff --git a/tests/functional/openlp_core/ui/test_firsttimeform.py b/tests/functional/openlp_core/ui/test_firsttimeform.py index b56a192d3..220e1fff6 100644 --- a/tests/functional/openlp_core/ui/test_firsttimeform.py +++ b/tests/functional/openlp_core/ui/test_firsttimeform.py @@ -25,40 +25,69 @@ Package to test the openlp.core.ui.firsttimeform package. import os import tempfile from unittest import TestCase -from unittest.mock import MagicMock, call, patch +from unittest.mock import MagicMock, call, patch, DEFAULT from openlp.core.common.path import Path from openlp.core.common.registry import Registry -from openlp.core.ui.firsttimeform import FirstTimeForm +from openlp.core.ui.firsttimeform import FirstTimeForm, ThemeListWidgetItem from tests.helpers.testmixin import TestMixin -FAKE_CONFIG = """ -[general] -base url = http://example.com/frw/ -[songs] -directory = songs -[bibles] -directory = bibles -[themes] -directory = themes +INVALID_CONFIG = """ +{ + "_comments": "The most recent version should be added to https://openlp.org/files/frw/download_3.0.json", + "_meta": { +} """ -FAKE_BROKEN_CONFIG = """ -[general] -base url = http://example.com/frw/ -[songs] -directory = songs -[bibles] -directory = bibles -""" -FAKE_INVALID_CONFIG = """ - -