From 1cf314007393dc2abff6bc7de6e599590e6892ec Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Wed, 1 Jan 2014 00:27:27 +0200 Subject: [PATCH] Reworked SongSelect branch after a major refactor in trunk. To recap: - Added a standalone CCLI SongSelect importer - Add icons to the import and export menu items - Move Theme import and export up with Settings --- openlp/core/lib/__init__.py | 1 + openlp/core/lib/historycombobox.py | 91 ++++ openlp/core/ui/__init__.py | 35 +- openlp/core/ui/mainwindow.py | 32 +- openlp/plugins/songs/forms/editsongdialog.py | 23 +- .../plugins/songs/forms/songselectdialog.py | 251 ++++++++++ openlp/plugins/songs/forms/songselectform.py | 473 ++++++++++++++++++ openlp/plugins/songs/songsplugin.py | 115 +++-- resources/images/general_back.png | Bin 0 -> 907 bytes resources/images/general_find.png | Bin 0 -> 942 bytes resources/images/openlp-2.qrc | 2 + 11 files changed, 934 insertions(+), 89 deletions(-) create mode 100644 openlp/core/lib/historycombobox.py create mode 100644 openlp/plugins/songs/forms/songselectdialog.py create mode 100755 openlp/plugins/songs/forms/songselectform.py create mode 100644 resources/images/general_back.png create mode 100644 resources/images/general_find.png diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index b5f2b1cee..d80978901 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -345,3 +345,4 @@ from .dockwidget import OpenLPDockWidget from .imagemanager import ImageManager from .renderer import Renderer from .mediamanageritem import MediaManagerItem +from .historycombobox import HistoryComboBox diff --git a/openlp/core/lib/historycombobox.py b/openlp/core/lib/historycombobox.py new file mode 100644 index 000000000..b6d06ff9f --- /dev/null +++ b/openlp/core/lib/historycombobox.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# 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; version 2 of the License. # +# # +# 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, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`~openlp.core.lib.historycombobox` module contains the HistoryComboBox widget +""" + +from PyQt4 import QtCore, QtGui + + +class HistoryComboBox(QtGui.QComboBox): + """ + The :class:`~openlp.core.lib.historycombobox.HistoryComboBox` widget emulates the QLineEdit ``returnPressed`` signal + for when the :kbd:`Enter` or :kbd:`Return` keys are pressed, and saves anything that is typed into the edit box into + its list. + """ + returnPressed = QtCore.pyqtSignal() + + def __init__(self, parent=None): + """ + Initialise the combo box, setting duplicates to False and the insert policy to insert items at the top. + + :param parent: The parent widget + """ + super().__init__(parent) + self.setDuplicatesEnabled(False) + self.setEditable(True) + self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) + self.setInsertPolicy(QtGui.QComboBox.InsertAtTop) + + def keyPressEvent(self, event): + """ + Override the inherited keyPressEvent method to emit the ``returnPressed`` signal and to save the current text to + the dropdown list. + + :param event: The keyboard event + """ + # Handle Enter and Return ourselves + if event.key() == QtCore.Qt.Key_Enter or event.key() == QtCore.Qt.Key_Return: + # Emit the returnPressed signal + self.returnPressed.emit() + # Save the current text to the dropdown list + if self.currentText() and self.findText(self.currentText()) == -1: + self.insertItem(0, self.currentText()) + # Let the parent handle any keypress events + super().keyPressEvent(event) + + def focusOutEvent(self, event): + """ + Override the inherited focusOutEvent to save the current text to the dropdown list. + + :param event: The focus event + """ + # Save the current text to the dropdown list + if self.currentText() and self.findText(self.currentText()) == -1: + self.insertItem(0, self.currentText()) + # Let the parent handle any keypress events + super().focusOutEvent(event) + + def getItems(self): + """ + Get all the items from the history + + :return: A list of strings + """ + return [self.itemText(i) for i in range(self.count())] diff --git a/openlp/core/ui/__init__.py b/openlp/core/ui/__init__.py index 677c65ab1..b2b29ebe9 100644 --- a/openlp/core/ui/__init__.py +++ b/openlp/core/ui/__init__.py @@ -29,6 +29,7 @@ """ The :mod:`ui` module provides the core user interface for OpenLP """ +from PyQt4 import QtGui class HideMode(object): @@ -77,6 +78,29 @@ class DisplayControllerType(object): Plugin = 2 +class SingleColumnTableWidget(QtGui.QTableWidget): + """ + Class to for a single column table widget to use for the verse table widget. + """ + + def __init__(self, parent): + """ + Constructor + """ + super(SingleColumnTableWidget, self).__init__(parent) + self.horizontalHeader().setVisible(False) + self.setColumnCount(1) + + def resizeEvent(self, event): + """ + Resize the first column together with the widget. + """ + QtGui.QTableWidget.resizeEvent(self, event) + if self.columnCount(): + self.setColumnWidth(0, event.size().width()) + self.resizeRowsToContents() + + from .firsttimeform import FirstTimeForm from .firsttimelanguageform import FirstTimeLanguageForm from .themelayoutform import ThemeLayoutForm @@ -102,8 +126,9 @@ from .servicemanager import ServiceManager from .thememanagerhelper import ThemeManagerHelper from .thememanager import ThemeManager -__all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager', - 'ThemeManager', 'MediaDockManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm', 'ThemeForm', - 'ThemeLayoutForm', 'FileRenameForm', 'StartTimeForm', 'MainDisplay', 'Display', 'ServiceNoteForm', - 'SlideController', 'DisplayController', 'GeneralTab', 'ThemesTab', 'AdvancedTab', 'PluginForm', - 'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController', 'ThemeManagerHelper'] +__all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideController', 'ServiceManager', 'ThemeForm', + 'ThemeManager', 'MediaDockManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm', + 'Display', 'ServiceNoteForm', 'ThemeLayoutForm', 'FileRenameForm', 'StartTimeForm', 'MainDisplay', + 'SlideController', 'DisplayController', 'GeneralTab', 'ThemesTab', 'AdvancedTab', 'PluginForm', + 'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController', 'ThemeManagerHelper', + 'SingleColumnTableWidget'] diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 46c318061..71bc99ff7 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -119,8 +119,10 @@ class Ui_MainWindow(object): self.recent_files_menu = QtGui.QMenu(self.file_menu) self.recent_files_menu.setObjectName('recentFilesMenu') self.file_import_menu = QtGui.QMenu(self.file_menu) + self.file_import_menu.setIcon(build_icon(u':/general/general_import.png')) self.file_import_menu.setObjectName('file_import_menu') self.file_export_menu = QtGui.QMenu(self.file_menu) + self.file_export_menu.setIcon(build_icon(u':/general/general_export.png')) self.file_export_menu.setObjectName('file_export_menu') # View Menu self.view_menu = QtGui.QMenu(self.menu_bar) @@ -230,7 +232,7 @@ class Ui_MainWindow(object): main_window, 'modeDefaultItem', checked=False, category=UiStrings().ViewMode, can_shortcuts=True) self.mode_setup_item = create_action(main_window, 'modeSetupItem', checked=False, category=UiStrings().ViewMode, can_shortcuts=True) - self.mode_live_item = create_action(main_window, 'modeLiveItem', checked=True, category=UiStrings().ViewMode, + self.mode_live_item = create_action(main_window, 'modeLiveItem', checked=True, category=UiStrings().ViewMode, can_shortcuts=True) self.mode_group = QtGui.QActionGroup(main_window) self.mode_group.addAction(self.mode_default_item) @@ -239,10 +241,10 @@ class Ui_MainWindow(object): self.mode_default_item.setChecked(True) action_list.add_category(UiStrings().Tools, CategoryOrder.standard_menu) self.tools_add_tool_item = create_action(main_window, - 'toolsAddToolItem', icon=':/tools/tools_add.png', + 'toolsAddToolItem', icon=':/tools/tools_add.png', category=UiStrings().Tools, can_shortcuts=True) self.tools_open_data_folder = create_action(main_window, - 'toolsOpenDataFolder', icon=':/general/general_open.png', + 'toolsOpenDataFolder', icon=':/general/general_open.png', category=UiStrings().Tools, can_shortcuts=True) self.tools_first_time_wizard = create_action(main_window, 'toolsFirstTimeWizard', icon=':/general/general_revert.png', @@ -268,24 +270,24 @@ class Ui_MainWindow(object): language_item = create_action(main_window, key, checked=qm_list[key] == saved_language) add_actions(self.language_group, [language_item]) self.settings_shortcuts_item = create_action(main_window, 'settingsShortcutsItem', - icon=':/system/system_configure_shortcuts.png', + icon=':/system/system_configure_shortcuts.png', category=UiStrings().Settings, can_shortcuts=True) # Formatting Tags were also known as display tags. self.formatting_tag_item = create_action(main_window, 'displayTagItem', - icon=':/system/tag_editor.png', category=UiStrings().Settings, + icon=':/system/tag_editor.png', category=UiStrings().Settings, can_shortcuts=True) self.settings_configure_item = create_action(main_window, 'settingsConfigureItem', - icon=':/system/system_settings.png', can_shortcuts=True, + icon=':/system/system_settings.png', can_shortcuts=True, category=UiStrings().Settings) # Give QT Extra Hint that this is the Preferences Menu Item self.settings_configure_item.setMenuRole(QtGui.QAction.PreferencesRole) - self.settings_import_item = create_action(main_window, 'settingsImportItem', + self.settings_import_item = create_action(main_window, 'settingsImportItem', category=UiStrings().Import, can_shortcuts=True) - self.settings_export_item = create_action(main_window, 'settingsExportItem', + self.settings_export_item = create_action(main_window, 'settingsExportItem', category=UiStrings().Export, can_shortcuts=True) action_list.add_category(UiStrings().Help, CategoryOrder.standard_menu) self.about_item = create_action(main_window, 'aboutItem', icon=':/system/system_about.png', - can_shortcuts=True, category=UiStrings().Help, + can_shortcuts=True, category=UiStrings().Help, triggers=self.on_about_item_clicked) # Give QT Extra Hint that this is an About Menu Item self.about_item.setMenuRole(QtGui.QAction.AboutRole) @@ -302,13 +304,13 @@ class Ui_MainWindow(object): self.web_site_item = create_action(main_window, 'webSiteItem', can_shortcuts=True, category=UiStrings().Help) # Shortcuts not connected to buttons or menu entries. self.search_shortcut_action = create_action(main_window, - 'searchShortcut', can_shortcuts=True, + 'searchShortcut', can_shortcuts=True, category=translate('OpenLP.MainWindow', 'General'), triggers=self.on_search_shortcut_triggered) - add_actions(self.file_import_menu, (self.settings_import_item, None, self.import_theme_item, - self.import_language_item)) - add_actions(self.file_export_menu, (self.settings_export_item, None, self.export_theme_item, - self.export_language_item)) + add_actions(self.file_import_menu, (self.settings_import_item, self.import_theme_item, + self.import_language_item, None)) + add_actions(self.file_export_menu, (self.settings_export_item, self.export_theme_item, + self.export_language_item, None)) add_actions(self.file_menu, (self.file_new_item, self.file_open_item, self.file_save_item, self.file_save_as_item, self.recent_files_menu.menuAction(), None, self.file_import_menu.menuAction(), self.file_export_menu.menuAction(), None, self.print_service_order_item, @@ -650,7 +652,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): 'Time Wizard?\n\nRe-running this wizard may make changes to your ' 'current OpenLP configuration and possibly add songs to your ' '#existing songs list and change your default theme.'), - QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No), QtGui.QMessageBox.No) if answer == QtGui.QMessageBox.No: diff --git a/openlp/plugins/songs/forms/editsongdialog.py b/openlp/plugins/songs/forms/editsongdialog.py index d0fda031e..71d7df8c0 100644 --- a/openlp/plugins/songs/forms/editsongdialog.py +++ b/openlp/plugins/songs/forms/editsongdialog.py @@ -32,6 +32,7 @@ from PyQt4 import QtCore, QtGui from openlp.core.common import UiStrings, translate from openlp.core.lib import build_icon from openlp.core.lib.ui import create_button_box, create_button +from openlp.core.ui import SingleColumnTableWidget from openlp.plugins.songs.lib.ui import SongStrings @@ -346,25 +347,3 @@ def create_combo_box(parent, name): combo_box.setInsertPolicy(QtGui.QComboBox.NoInsert) combo_box.setObjectName(name) return combo_box - - -class SingleColumnTableWidget(QtGui.QTableWidget): - """ - Class to for a single column table widget to use for the verse table widget. - """ - def __init__(self, parent): - """ - Constructor - """ - super(SingleColumnTableWidget, self).__init__(parent) - self.horizontalHeader().setVisible(False) - self.setColumnCount(1) - - def resizeEvent(self, event): - """ - Resize the first column together with the widget. - """ - QtGui.QTableWidget.resizeEvent(self, event) - if self.columnCount(): - self.setColumnWidth(0, event.size().width()) - self.resizeRowsToContents() diff --git a/openlp/plugins/songs/forms/songselectdialog.py b/openlp/plugins/songs/forms/songselectdialog.py new file mode 100644 index 000000000..5eda9c497 --- /dev/null +++ b/openlp/plugins/songs/forms/songselectdialog.py @@ -0,0 +1,251 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# 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; version 2 of the License. # +# # +# 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, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`~openlp.plugins.songs.forms.songselectdialog` module contains the user interface code for the dialog +""" + +from PyQt4 import QtCore, QtGui + +from openlp.core.lib import HistoryComboBox, translate, build_icon +from openlp.core.ui import SingleColumnTableWidget + + +class Ui_SongSelectDialog(object): + """ + The actual Qt components that make up the dialog. + """ + def setup_ui(self, songselect_dialog): + songselect_dialog.setObjectName('songselect_dialog') + songselect_dialog.resize(616, 378) + self.songselect_layout = QtGui.QVBoxLayout(songselect_dialog) + self.songselect_layout.setSpacing(0) + self.songselect_layout.setMargin(0) + self.songselect_layout.setObjectName('songselect_layout') + self.stacked_widget = QtGui.QStackedWidget(songselect_dialog) + self.stacked_widget.setObjectName('stacked_widget') + self.login_page = QtGui.QWidget() + self.login_page.setObjectName('login_page') + self.login_layout = QtGui.QFormLayout(self.login_page) + self.login_layout.setContentsMargins(120, 100, 120, 100) + self.login_layout.setSpacing(8) + self.login_layout.setObjectName('login_layout') + self.notice_layout = QtGui.QHBoxLayout() + self.notice_layout.setObjectName('notice_layout') + self.notice_label = QtGui.QLabel(self.login_page) + self.notice_label.setWordWrap(True) + self.notice_label.setObjectName('notice_label') + self.notice_layout.addWidget(self.notice_label) + self.login_layout.setLayout(0, QtGui.QFormLayout.SpanningRole, self.notice_layout) + self.username_label = QtGui.QLabel(self.login_page) + self.username_label.setObjectName('usernameLabel') + self.login_layout.setWidget(1, QtGui.QFormLayout.LabelRole, self.username_label) + self.username_edit = QtGui.QLineEdit(self.login_page) + self.username_edit.setObjectName('usernameEdit') + self.login_layout.setWidget(1, QtGui.QFormLayout.FieldRole, self.username_edit) + self.password_label = QtGui.QLabel(self.login_page) + self.password_label.setObjectName('passwordLabel') + self.login_layout.setWidget(2, QtGui.QFormLayout.LabelRole, self.password_label) + self.password_edit = QtGui.QLineEdit(self.login_page) + self.password_edit.setEchoMode(QtGui.QLineEdit.Password) + self.password_edit.setObjectName('passwordEdit') + self.login_layout.setWidget(2, QtGui.QFormLayout.FieldRole, self.password_edit) + self.save_password_checkbox = QtGui.QCheckBox(self.login_page) + self.save_password_checkbox.setTristate(False) + self.save_password_checkbox.setObjectName('save_password_checkbox') + self.login_layout.setWidget(3, QtGui.QFormLayout.FieldRole, self.save_password_checkbox) + self.login_button_layout = QtGui.QHBoxLayout() + self.login_button_layout.setSpacing(8) + self.login_button_layout.setContentsMargins(0, -1, -1, -1) + self.login_button_layout.setObjectName('login_button_layout') + self.login_spacer = QtGui.QWidget(self.login_page) + self.login_spacer.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.login_spacer.setObjectName('login_spacer') + self.login_button_layout.addWidget(self.login_spacer) + self.login_progress_bar = QtGui.QProgressBar(self.login_page) + self.login_progress_bar.setMinimum(0) + self.login_progress_bar.setMaximum(3) + self.login_progress_bar.setValue(0) + self.login_progress_bar.setMinimumWidth(200) + self.login_progress_bar.setVisible(False) + self.login_button_layout.addWidget(self.login_progress_bar) + self.login_button = QtGui.QPushButton(self.login_page) + self.login_button.setIcon(build_icon(':/songs/song_author_edit.png')) + self.login_button.setObjectName('login_button') + self.login_button_layout.addWidget(self.login_button) + self.login_layout.setLayout(4, QtGui.QFormLayout.SpanningRole, self.login_button_layout) + self.stacked_widget.addWidget(self.login_page) + self.search_page = QtGui.QWidget() + self.search_page.setObjectName('search_page') + self.search_layout = QtGui.QVBoxLayout(self.search_page) + self.search_layout.setSpacing(8) + self.search_layout.setMargin(8) + self.search_layout.setObjectName('search_layout') + self.search_input_layout = QtGui.QHBoxLayout() + self.search_input_layout.setSpacing(8) + self.search_input_layout.setObjectName('search_input_layout') + self.search_label = QtGui.QLabel(self.search_page) + self.search_label.setObjectName('search_label') + self.search_input_layout.addWidget(self.search_label) + self.search_combobox = HistoryComboBox(self.search_page) + self.search_combobox.setObjectName('search_combobox') + self.search_input_layout.addWidget(self.search_combobox) + self.search_button = QtGui.QPushButton(self.search_page) + self.search_button.setIcon(build_icon(':/general/general_find.png')) + self.search_button.setObjectName('search_button') + self.search_input_layout.addWidget(self.search_button) + self.search_layout.addLayout(self.search_input_layout) + self.search_progress_bar = QtGui.QProgressBar(self.search_page) + self.search_progress_bar.setMinimum(0) + self.search_progress_bar.setMaximum(3) + self.search_progress_bar.setValue(0) + self.search_progress_bar.setVisible(False) + self.search_layout.addWidget(self.search_progress_bar) + self.search_results_widget = QtGui.QListWidget(self.search_page) + self.search_results_widget.setProperty("showDropIndicator", False) + self.search_results_widget.setAlternatingRowColors(True) + self.search_results_widget.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) + self.search_results_widget.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) + self.search_results_widget.setObjectName('search_results_widget') + self.search_layout.addWidget(self.search_results_widget) + self.result_count_label = QtGui.QLabel(self.search_page) + self.result_count_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignCenter) + self.result_count_label.setObjectName('result_count_label') + self.search_layout.addWidget(self.result_count_label) + self.view_layout = QtGui.QHBoxLayout() + self.view_layout.setSpacing(8) + self.view_layout.setObjectName('view_layout') + self.logout_button = QtGui.QPushButton(self.search_page) + self.logout_button.setIcon(build_icon(':/songs/song_author_edit.png')) + self.view_layout.addWidget(self.logout_button) + self.view_spacer = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.view_layout.addItem(self.view_spacer) + self.view_button = QtGui.QPushButton(self.search_page) + self.view_button.setIcon(build_icon(':/songs/song_search_all.png')) + self.view_button.setObjectName('view_button') + self.view_layout.addWidget(self.view_button) + self.search_layout.addLayout(self.view_layout) + self.stacked_widget.addWidget(self.search_page) + self.song_page = QtGui.QWidget() + self.song_page.setObjectName('song_page') + self.song_layout = QtGui.QGridLayout(self.song_page) + self.song_layout.setMargin(8) + self.song_layout.setSpacing(8) + self.song_layout.setObjectName('song_layout') + self.title_label = QtGui.QLabel(self.song_page) + self.title_label.setObjectName('title_label') + self.song_layout.addWidget(self.title_label, 0, 0, 1, 1) + self.title_edit = QtGui.QLineEdit(self.song_page) + self.title_edit.setReadOnly(True) + self.title_edit.setObjectName('title_edit') + self.song_layout.addWidget(self.title_edit, 0, 1, 1, 1) + self.authors_label = QtGui.QLabel(self.song_page) + self.authors_label.setObjectName('authors_label') + self.song_layout.addWidget(self.authors_label, 0, 2, 1, 1) + self.author_list_widget = QtGui.QListWidget(self.song_page) + self.author_list_widget.setObjectName('author_list_widget') + self.song_layout.addWidget(self.author_list_widget, 0, 3, 3, 1) + self.copyright_label = QtGui.QLabel(self.song_page) + self.copyright_label.setObjectName('copyright_label') + self.song_layout.addWidget(self.copyright_label, 1, 0, 1, 1) + self.copyright_edit = QtGui.QLineEdit(self.song_page) + self.copyright_edit.setReadOnly(True) + self.copyright_edit.setObjectName('copyright_edit') + self.song_layout.addWidget(self.copyright_edit, 1, 1, 1, 1) + self.ccli_label = QtGui.QLabel(self.song_page) + self.ccli_label.setObjectName('ccli_label') + self.song_layout.addWidget(self.ccli_label, 2, 0, 1, 1) + self.ccli_edit = QtGui.QLineEdit(self.song_page) + self.ccli_edit.setReadOnly(True) + self.ccli_edit.setObjectName('ccli_edit') + self.song_layout.addWidget(self.ccli_edit, 2, 1, 1, 1) + self.lyrics_label = QtGui.QLabel(self.song_page) + self.lyrics_label.setAlignment(QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) + self.lyrics_label.setObjectName('lyrics_label') + self.song_layout.addWidget(self.lyrics_label, 3, 0, 1, 1) + self.lyrics_table_widget = SingleColumnTableWidget(self.song_page) + self.lyrics_table_widget.setObjectName('lyrics_table_widget') + self.lyrics_table_widget.setRowCount(0) + self.song_layout.addWidget(self.lyrics_table_widget, 3, 1, 1, 3) + self.song_progress_bar = QtGui.QProgressBar(self.song_page) + self.song_progress_bar.setMinimum(0) + self.song_progress_bar.setMaximum(3) + self.song_progress_bar.setValue(0) + self.song_progress_bar.setVisible(False) + self.song_layout.addWidget(self.song_progress_bar, 4, 0, 1, 4) + self.import_layout = QtGui.QHBoxLayout() + self.import_layout.setObjectName('import_layout') + self.back_button = QtGui.QPushButton(self.song_page) + self.back_button.setIcon(build_icon(':/general/general_back.png')) + self.back_button.setObjectName('back_button') + self.import_layout.addWidget(self.back_button) + self.import_spacer = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.import_layout.addItem(self.import_spacer) + self.import_button = QtGui.QPushButton(self.song_page) + self.import_button.setIcon(build_icon(':/general/general_import.png')) + self.import_button.setObjectName('import_button') + self.import_layout.addWidget(self.import_button) + self.song_layout.addLayout(self.import_layout, 5, 0, 1, 5) + self.stacked_widget.addWidget(self.song_page) + self.songselect_layout.addWidget(self.stacked_widget) + self.username_label.setBuddy(self.username_edit) + self.password_label.setBuddy(self.password_edit) + self.title_label.setBuddy(self.title_edit) + self.authors_label.setBuddy(self.author_list_widget) + self.copyright_label.setBuddy(self.copyright_edit) + self.ccli_label.setBuddy(self.ccli_edit) + self.lyrics_label.setBuddy(self.lyrics_table_widget) + + self.retranslate_ui(songselect_dialog) + self.stacked_widget.setCurrentIndex(0) + + def retranslate_ui(self, songselect_dialog): + """ + Translate the GUI. + """ + songselect_dialog.setWindowTitle(translate('SongsPlugin.SongSelectForm', 'CCLI SongSelect Importer')) + self.notice_label.setText( + translate('SongsPlugin.SongSelectForm', 'Note: ' + 'An Internet connection is required in order to import songs from CCLI SongSelect.') + ) + self.username_label.setText(translate('SongsPlugin.SongSelectForm', 'Username:')) + self.password_label.setText(translate('SongsPlugin.SongSelectForm', 'Password:')) + self.save_password_checkbox.setText(translate('SongsPlugin.SongSelectForm', 'Save username and password')) + self.login_button.setText(translate('SongsPlugin.SongSelectForm', 'Login')) + self.search_label.setText(translate('SongsPlugin.SongSelectForm', 'Search Text:')) + self.search_button.setText(translate('SongsPlugin.SongSelectForm', 'Search')) + self.result_count_label.setText(translate('SongsPlugin.SongSelectForm', 'Found %s song(s)') % 0) + self.logout_button.setText(translate('SongsPlugin.SongSelectForm', 'Logout')) + self.view_button.setText(translate('SongsPlugin.SongSelectForm', 'View')) + self.title_label.setText(translate('SongsPlugin.SongSelectForm', 'Title:')) + self.authors_label.setText(translate('SongsPlugin.SongSelectForm', 'Author(s):')) + self.copyright_label.setText(translate('SongsPlugin.SongSelectForm', 'Copyright:')) + self.ccli_label.setText(translate('SongsPlugin.SongSelectForm', 'CCLI Number:')) + self.lyrics_label.setText(translate('SongsPlugin.SongSelectForm', 'Lyrics:')) + self.back_button.setText(translate('SongsPlugin.SongSelectForm', 'Back')) + self.import_button.setText(translate('SongsPlugin.SongSelectForm', 'Import')) diff --git a/openlp/plugins/songs/forms/songselectform.py b/openlp/plugins/songs/forms/songselectform.py new file mode 100755 index 000000000..f9efb70bc --- /dev/null +++ b/openlp/plugins/songs/forms/songselectform.py @@ -0,0 +1,473 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, # +# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. # +# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, # +# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, # +# Frode Woldsund, Martin Zibricky, Patrick Zimmermann # +# --------------------------------------------------------------------------- # +# 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; version 2 of the License. # +# # +# 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, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`~openlp.plugins.songs.forms.songselectform` module contains the GUI for the SongSelect importer +""" + +import logging +from http.cookiejar import CookieJar +from urllib.parse import urlencode +from urllib.request import HTTPCookieProcessor, HTTPError, build_opener +from html.parser import HTMLParser +from time import sleep + +from PyQt4 import QtCore, QtGui +from bs4 import BeautifulSoup, NavigableString +from openlp.core import Settings + +from openlp.core.common import Registry +from openlp.core.lib import translate +from openlp.plugins.songs.lib import VerseType, clean_song +from openlp.plugins.songs.forms.songselectdialog import Ui_SongSelectDialog +from openlp.plugins.songs.lib.db import Author, Song +from openlp.plugins.songs.lib.xml import SongXML + +USER_AGENT = 'Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; GT-I9000 ' \ + 'Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 ' \ + 'Mobile Safari/534.30' +BASE_URL = 'https://mobile.songselect.com' +LOGIN_URL = BASE_URL + '/account/login' +LOGOUT_URL = BASE_URL + '/account/logout' +SEARCH_URL = BASE_URL + '/search/results' + +log = logging.getLogger(__name__) + + +class SearchWorker(QtCore.QObject): + """ + Run the actual SongSelect search, and notify the GUI when we find each song. + """ + show_info = QtCore.pyqtSignal(str, str) + found_song = QtCore.pyqtSignal(dict) + finished = QtCore.pyqtSignal(list) + quit = QtCore.pyqtSignal() + + def __init__(self, opener, params): + super().__init__() + self.opener = opener + self.params = params + self.html_parser = HTMLParser() + + def _search_and_parse_results(self, params): + params = urlencode(params) + results_page = BeautifulSoup(self.opener.open(SEARCH_URL + '?' + params).read(), 'lxml') + search_results = results_page.find_all('li', 'result pane') + songs = [] + for result in search_results: + song = { + 'title': self.html_parser.unescape(result.find('h3').string), + 'authors': [self.html_parser.unescape(author.string) for author in result.find_all('li')], + 'link': BASE_URL + result.find('a')['href'] + } + self.found_song.emit(song) + songs.append(song) + return songs + + def start(self): + """ + Run a search and then parse the results page of the search. + """ + songs = self._search_and_parse_results(self.params) + search_results = [] + self.params['page'] = 1 + total = 0 + while songs: + search_results.extend(songs) + self.params['page'] += 1 + total += len(songs) + if total >= 1000: + self.show_info.emit( + translate('SongsPlugin.SongSelectForm', 'More than 1000 results'), + translate('SongsPlugin.SongSelectForm', 'Your search has returned more than 1000 results, it has ' + 'been stopped. Please refine your search to fetch better ' + 'results.')) + break + songs = self._search_and_parse_results(self.params) + self.finished.emit(search_results) + self.quit.emit() + + +class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog): + """ + The :class:`SongSelectForm` class is the SongSelect dialog. + """ + + def __init__(self, parent=None, plugin=None, db_manager=None): + QtGui.QDialog.__init__(self, parent) + self.setup_ui(self) + self.thread = None + self.worker = None + self.song_count = 0 + self.song = None + self.plugin = plugin + self.db_manager = db_manager + self.html_parser = HTMLParser() + self.opener = build_opener(HTTPCookieProcessor(CookieJar())) + self.opener.addheaders = [('User-Agent', USER_AGENT)] + self.save_password_checkbox.toggled.connect(self.on_save_password_checkbox_toggled) + self.login_button.clicked.connect(self.on_login_button_clicked) + self.search_button.clicked.connect(self.on_search_button_clicked) + self.search_combobox.returnPressed.connect(self.on_search_button_clicked) + self.logout_button.clicked.connect(self.done) + self.search_results_widget.itemDoubleClicked.connect(self.on_search_results_widget_double_clicked) + self.search_results_widget.itemSelectionChanged.connect(self.on_search_results_widget_selection_changed) + self.view_button.clicked.connect(self.on_view_button_clicked) + self.back_button.clicked.connect(self.on_back_button_clicked) + self.import_button.clicked.connect(self.on_import_button_clicked) + + def exec_(self): + """ + Execute the dialog. This method sets everything back to its initial + values. + """ + self.stacked_widget.setCurrentIndex(0) + self.username_edit.setEnabled(True) + self.password_edit.setEnabled(True) + self.save_password_checkbox.setEnabled(True) + self.search_combobox.clearEditText() + self.search_combobox.clear() + self.search_results_widget.clear() + self.view_button.setEnabled(False) + if Settings().contains(self.plugin.settings_section + '/songselect password'): + self.username_edit.setText(Settings().value(self.plugin.settings_section + '/songselect username')) + self.password_edit.setText(Settings().value(self.plugin.settings_section + '/songselect password')) + self.save_password_checkbox.setChecked(True) + if Settings().contains(self.plugin.settings_section + '/songselect searches'): + self.search_combobox.addItems( + Settings().value(self.plugin.settings_section + '/songselect searches').split('|')) + self.username_edit.setFocus() + return QtGui.QDialog.exec_(self) + + def done(self, r): + """ + Log out of SongSelect. + + :param r: The result of the dialog. + """ + log.debug('Closing SongSelectForm') + if self.stacked_widget.currentIndex() > 0: + progress_dialog = QtGui.QProgressDialog( + translate('SongsPlugin.SongSelectForm', 'Logging out...'), '', 0, 2, self) + progress_dialog.setWindowModality(QtCore.Qt.WindowModal) + progress_dialog.setCancelButton(None) + progress_dialog.setValue(1) + progress_dialog.show() + progress_dialog.setFocus() + self.main_window.application.process_events() + sleep(0.5) + self.main_window.application.process_events() + self.opener.open(LOGOUT_URL) + self.main_window.application.process_events() + progress_dialog.setValue(2) + return QtGui.QDialog.done(self, r) + + def _get_main_window(self): + if not hasattr(self, '_main_window'): + self._main_window = Registry().get('main_window') + return self._main_window + + main_window = property(_get_main_window) + + def _view_song(self, current_item): + if not current_item: + return + else: + current_item = current_item.data(QtCore.Qt.UserRole) + self.song_progress_bar.setVisible(True) + self.import_button.setEnabled(False) + self.back_button.setEnabled(False) + self.title_edit.setText('') + self.title_edit.setEnabled(False) + self.copyright_edit.setText('') + self.copyright_edit.setEnabled(False) + self.ccli_edit.setText('') + self.ccli_edit.setEnabled(False) + self.author_list_widget.clear() + self.author_list_widget.setEnabled(False) + self.lyrics_table_widget.clear() + self.lyrics_table_widget.setRowCount(0) + self.lyrics_table_widget.setEnabled(False) + self.stacked_widget.setCurrentIndex(2) + song = {} + for key, value in current_item.items(): + song[key] = value + self.song_progress_bar.setValue(1) + self.main_window.application.process_events() + song_page = BeautifulSoup(self.opener.open(song['link']).read(), 'lxml') + self.song_progress_bar.setValue(2) + self.main_window.application.process_events() + try: + lyrics_page = BeautifulSoup(self.opener.open(song['link'] + '/lyrics').read(), 'lxml') + except HTTPError: + lyrics_page = None + self.song_progress_bar.setValue(3) + self.main_window.application.process_events() + song['copyright'] = '/'.join([li.string for li in song_page.find('ul', 'copyright').find_all('li')]) + song['copyright'] = self.html_parser.unescape(song['copyright']) + song['ccli_number'] = song_page.find('ul', 'info').find('li').string.split(':')[1].strip() + song['verses'] = [] + if lyrics_page: + verses = lyrics_page.find('section', 'lyrics').find_all('p') + verse_labels = lyrics_page.find('section', 'lyrics').find_all('h3') + for counter in range(len(verses)): + verse = {'label': verse_labels[counter].string, 'lyrics': ''} + for v in verses[counter].contents: + if isinstance(v, NavigableString): + verse['lyrics'] = verse['lyrics'] + v.string + else: + verse['lyrics'] += '\n' + verse['lyrics'] = verse['lyrics'].strip(' \n\r\t') + song['verses'].append(self.html_parser.unescape(verse)) + self.title_edit.setText(song['title']) + self.copyright_edit.setText(song['copyright']) + self.ccli_edit.setText(song['ccli_number']) + for author in song['authors']: + QtGui.QListWidgetItem(self.html_parser.unescape(author), self.author_list_widget) + for counter, verse in enumerate(song['verses']): + log.debug('Verse type: %s', verse['label']) + self.lyrics_table_widget.setRowCount(self.lyrics_table_widget.rowCount() + 1) + item = QtGui.QTableWidgetItem(verse['lyrics']) + item.setData(QtCore.Qt.UserRole, verse['label']) + item.setFlags(item.flags() ^ QtCore.Qt.ItemIsEditable) + self.lyrics_table_widget.setItem(counter, 0, item) + self.lyrics_table_widget.setVerticalHeaderLabels([verse['label'] for verse in song['verses']]) + self.lyrics_table_widget.resizeRowsToContents() + self.title_edit.setEnabled(True) + self.copyright_edit.setEnabled(True) + self.ccli_edit.setEnabled(True) + self.author_list_widget.setEnabled(True) + self.lyrics_table_widget.setEnabled(True) + self.lyrics_table_widget.repaint() + self.import_button.setEnabled(True) + self.back_button.setEnabled(True) + self.song_progress_bar.setVisible(False) + self.song_progress_bar.setValue(0) + self.song = song + self.main_window.application.process_events() + + def on_save_password_checkbox_toggled(self, checked): + """ + Show a warning dialog when the user toggles the save checkbox on or off. + + :param checked: If the combobox is checked or not + """ + if checked and self.login_page.isVisible(): + answer = QtGui.QMessageBox.question( + self, translate('SongsPlugin.SongSelectForm', 'Save Username and Password'), + translate('SongsPlugin.SongSelectForm', 'WARNING: Saving your username and password is INSECURE, your ' + 'password is stored in PLAIN TEXT. Click Yes to save your ' + 'password or No to cancel this.'), + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No), QtGui.QMessageBox.No) + if answer == QtGui.QMessageBox.No: + self.save_password_checkbox.setChecked(False) + + def on_login_button_clicked(self): + """ + Log the user in to SongSelect. + """ + self.username_edit.setEnabled(False) + self.password_edit.setEnabled(False) + self.save_password_checkbox.setEnabled(False) + self.login_button.setEnabled(False) + self.login_spacer.setVisible(False) + self.login_progress_bar.setVisible(True) + self.login_progress_bar.setValue(1) + self.main_window.application.process_events() + login_page = BeautifulSoup(self.opener.open(LOGIN_URL).read(), 'lxml') + self.login_progress_bar.setValue(2) + self.main_window.application.process_events() + token_input = login_page.find('input', attrs={'name': '__RequestVerificationToken'}) + data = urlencode({ + '__RequestVerificationToken': token_input['value'], + 'UserName': self.username_edit.text(), + 'Password': self.password_edit.text(), + 'RememberMe': 'false' + }) + posted_page = BeautifulSoup(self.opener.open(LOGIN_URL, data.encode('utf-8')).read(), 'lxml') + self.login_progress_bar.setValue(3) + self.main_window.application.process_events() + if posted_page.find('input', attrs={'name': '__RequestVerificationToken'}): + QtGui.QMessageBox.critical( + self, + translate('SongsPlugin.SongSelectForm', 'Error Logging In'), + translate('SongsPlugin.SongSelectForm', + 'There was a problem logging in, perhaps your username or password is incorrect?') + ) + else: + if self.save_password_checkbox.isChecked(): + Settings().setValue(self.plugin.settings_section + '/songselect username', self.username_edit.text()) + Settings().setValue(self.plugin.settings_section + '/songselect password', self.password_edit.text()) + else: + Settings().remove(self.plugin.settings_section + '/songselect username') + Settings().remove(self.plugin.settings_section + '/songselect password') + self.stacked_widget.setCurrentIndex(1) + self.login_progress_bar.setVisible(False) + self.login_progress_bar.setValue(0) + self.login_spacer.setVisible(True) + self.login_button.setEnabled(True) + self.search_combobox.setFocus() + self.main_window.application.process_events() + + def on_search_button_clicked(self): + """ + Run a search on SongSelect. + """ + self.view_button.setEnabled(False) + self.search_button.setEnabled(False) + self.search_progress_bar.setVisible(True) + self.search_progress_bar.setMinimum(0) + self.search_progress_bar.setMaximum(0) + self.search_progress_bar.setValue(0) + self.search_results_widget.clear() + self.result_count_label.setText(translate('SongsPlugin.SongSelectForm', 'Found %s song(s)') % self.song_count) + self.main_window.application.process_events() + self.song_count = 0 + search_history = self.search_combobox.getItems() + Settings().setValue(self.plugin.settings_section + '/songselect searches', '|'.join(search_history)) + + # Create thread and run search + self.thread = QtCore.QThread() + self.worker = SearchWorker(self.opener, {'SearchTerm': self.search_combobox.currentText(), + 'allowredirect': 'false'}) + self.worker.moveToThread(self.thread) + self.thread.started.connect(self.worker.start) + self.worker.show_info.connect(self.on_search_show_info) + self.worker.found_song.connect(self.on_search_found_song) + self.worker.finished.connect(self.on_search_finished) + self.worker.quit.connect(self.thread.quit) + self.worker.quit.connect(self.worker.deleteLater) + self.thread.finished.connect(self.thread.deleteLater) + self.thread.start() + + def on_search_show_info(self, title, message): + """ + Show an informational message from the search thread + :param title: + :param message: + """ + QtGui.QMessageBox.information(self, title, message) + + def on_search_found_song(self, song): + """ + Add a song to the list when one is found. + :param song: + """ + log.debug('SongSelect (title = "%s"), (link = "%s")', song['title'], song['link']) + self.song_count += 1 + self.result_count_label.setText(translate('SongsPlugin.SongSelectForm', 'Found %s song(s)') % self.song_count) + item_title = song['title'] + ' (' + ', '.join(song['authors']) + ')' + song_item = QtGui.QListWidgetItem(item_title, self.search_results_widget) + song_item.setData(QtCore.Qt.UserRole, song) + + def on_search_finished(self, songs): + """ + Slot which is called when the search is completed. + :param songs: + """ + self.main_window.application.process_events() + self.search_progress_bar.setVisible(False) + self.search_button.setEnabled(True) + self.main_window.application.process_events() + + def on_search_results_widget_selection_changed(self): + """ + Enable or disable the view button when the selection changes. + """ + self.view_button.setEnabled(len(self.search_results_widget.selectedItems()) > 0) + + def on_view_button_clicked(self): + """ + View a song from SongSelect. + """ + self._view_song(self.search_results_widget.currentItem()) + + def on_search_results_widget_double_clicked(self, current_item): + """ + View a song from SongSelect + :param current_item: + """ + self._view_song(current_item) + + def on_back_button_clicked(self): + """ + Go back to the search page. + """ + self.stacked_widget.setCurrentIndex(1) + self.search_combobox.setFocus() + + def on_import_button_clicked(self): + """ + Import a song from SongSelect. + """ + song = Song.populate( + title=self.song['title'], + copyright=self.song['copyright'], + ccli_number=self.song['ccli_number'] + ) + song_xml = SongXML() + verse_order = [] + for verse in self.song['verses']: + verse_type, verse_number = verse['label'].split(' ')[:2] + verse_type = VerseType.from_loose_input(verse_type) + verse_number = int(verse_number) + song_xml.add_verse_to_lyrics( + VerseType.tags[verse_type], + verse_number, + verse['lyrics'] + ) + verse_order.append('%s%s' % (VerseType.tags[verse_type], verse_number)) + song.verse_order = ' '.join(verse_order) + song.lyrics = song_xml.extract_xml() + clean_song(self.db_manager, song) + self.db_manager.save_object(song) + song.authors = [] + for author_name in self.song['authors']: + #author_name = unicode(author_name) + author = self.db_manager.get_object_filtered(Author, Author.display_name == author_name) + if not author: + author = Author.populate( + first_name=author_name.rsplit(' ', 1)[0], + last_name=author_name.rsplit(' ', 1)[1], + display_name=author_name + ) + song.authors.append(author) + self.db_manager.save_object(song) + question_dialog = QtGui.QMessageBox() + question_dialog.setWindowTitle(translate('SongsPlugin.SongSelectForm', 'Song Imported')) + question_dialog.setText(translate('SongsPlugin.SongSelectForm', 'Your song has been imported, would you like ' + 'to exit now, or import more songs?')) + question_dialog.addButton(QtGui.QPushButton(translate('SongsPlugin.SongSelectForm', 'Import More Songs')), + QtGui.QMessageBox.YesRole) + question_dialog.addButton(QtGui.QPushButton(translate('SongsPlugin.SongSelectForm', 'Exit Now')), + QtGui.QMessageBox.NoRole) + if question_dialog.exec_() == QtGui.QMessageBox.Yes: + self.on_back_button_clicked() + else: + self.main_window.application.process_events() + self.done(QtGui.QDialog.Accepted) diff --git a/openlp/plugins/songs/songsplugin.py b/openlp/plugins/songs/songsplugin.py index 36fd92237..4c6dc8106 100644 --- a/openlp/plugins/songs/songsplugin.py +++ b/openlp/plugins/songs/songsplugin.py @@ -38,11 +38,13 @@ import sqlite3 from PyQt4 import QtCore, QtGui -from openlp.core.common import UiStrings, translate +from openlp.core.common import UiStrings, Registry, translate from openlp.core.lib import Plugin, StringContent, build_icon from openlp.core.lib.db import Manager from openlp.core.lib.ui import create_action from openlp.core.utils.actions import ActionList +from openlp.plugins.songs.forms.duplicatesongremovalform import DuplicateSongRemovalForm +from openlp.plugins.songs.forms.songselectform import SongSelectForm from openlp.plugins.songs.lib import clean_song, upgrade from openlp.plugins.songs.lib.db import init_schema, Song from openlp.plugins.songs.lib.mediaitem import SongSearch @@ -50,30 +52,29 @@ from openlp.plugins.songs.lib.importer import SongFormat from openlp.plugins.songs.lib.olpimport import OpenLPSongImport from openlp.plugins.songs.lib.mediaitem import SongMediaItem from openlp.plugins.songs.lib.songstab import SongsTab -from openlp.plugins.songs.forms.duplicatesongremovalform import DuplicateSongRemovalForm log = logging.getLogger(__name__) __default_settings__ = { - 'songs/db type': 'sqlite', - 'songs/last search type': SongSearch.Entire, - 'songs/last import type': SongFormat.OpenLyrics, - 'songs/update service on edit': False, - 'songs/search as type': False, - 'songs/add song from service': True, - 'songs/display songbar': True, - 'songs/last directory import': '', - 'songs/last directory export': '' - } + 'songs/db type': 'sqlite', + 'songs/last search type': SongSearch.Entire, + 'songs/last import type': SongFormat.OpenLyrics, + 'songs/update service on edit': False, + 'songs/search as type': False, + 'songs/add song from service': True, + 'songs/display songbar': True, + 'songs/last directory import': '', + 'songs/last directory export': '', + 'songs/songselect username': '', + 'songs/songselect password': '', + 'songs/songselect searches': '' +} class SongsPlugin(Plugin): """ - This is the number 1 plugin, if importance were placed on any - plugins. This plugin enables the user to create, edit and display - songs. Songs are divided into verses, and the verse order can be - specified. Authors, topics and song books can be assigned to songs - as well. + This plugin enables the user to create, edit and display songs. Songs are divided into verses, and the verse order + can be specified. Authors, topics and song books can be assigned to songs as well. """ log.info('Song Plugin loaded') @@ -86,6 +87,7 @@ class SongsPlugin(Plugin): self.weight = -10 self.icon_path = ':/plugins/plugin_songs.png' self.icon = build_icon(self.icon_path) + self.songselect_form = None def check_pre_conditions(self): """ @@ -94,8 +96,12 @@ class SongsPlugin(Plugin): return self.manager.session is not None def initialise(self): + """ + Initialise the plugin + """ log.info('Songs Initialising') super(SongsPlugin, self).initialise() + self.songselect_form = SongSelectForm(Registry().get('main_window'), self, self.manager) self.song_import_item.setVisible(True) self.song_export_item.setVisible(True) self.tools_reindex_item.setVisible(True) @@ -108,28 +114,27 @@ class SongsPlugin(Plugin): def add_import_menu_item(self, import_menu): """ - Give the Songs plugin the opportunity to add items to the - **Import** menu. + Give the Songs plugin the opportunity to add items to the **Import** menu. - ``import_menu`` - The actual **Import** menu item, so that your actions can - use it as their parent. + :param import_menu: The actual **Import** menu item, so that your actions can use it as their parent. """ # Main song import menu item - will eventually be the only one - self.song_import_item = create_action(import_menu, 'songImportItem', - text=translate('SongsPlugin', '&Song'), - tooltip=translate('SongsPlugin', 'Import songs using the import wizard.'), - triggers=self.on_song_import_item_clicked) + self.song_import_item = create_action(import_menu, 'songImportItem', text=translate('SongsPlugin', '&Song'), + tooltip=translate('SongsPlugin', 'Import songs using the import wizard.'), + triggers=self.on_song_import_item_clicked) import_menu.addAction(self.song_import_item) + self.import_songselect_item = create_action( + import_menu, 'import_songselect_item', text=translate('SongsPlugin', 'CCLI SongSelect'), + statustip=translate('SongsPlugin', 'Import songs from CCLI\'s SongSelect service.'), + triggers=self.on_import_songselect_item_triggered + ) + import_menu.addAction(self.import_songselect_item) def add_export_menu_Item(self, export_menu): """ - Give the Songs plugin the opportunity to add items to the - **Export** menu. + Give the Songs plugin the opportunity to add items to the **Export** menu. - ``export_menu`` - The actual **Export** menu item, so that your actions can - use it as their parent. + :param export_menu: The actual **Export** menu item, so that your actions can use it as their parent. """ # Main song import menu item - will eventually be the only one self.song_export_item = create_action(export_menu, 'songExportItem', @@ -140,12 +145,9 @@ class SongsPlugin(Plugin): def add_tools_menu_item(self, tools_menu): """ - Give the Songs plugin the opportunity to add items to the - **Tools** menu. + Give the Songs plugin the opportunity to add items to the **Tools** menu. - ``tools_menu`` - The actual **Tools** menu item, so that your actions can - use it as their parent. + :param tools_menu: The actual **Tools** menu item, so that your actions can use it as their parent. """ log.info('add tools menu') self.tools_reindex_item = create_action(tools_menu, 'toolsReindexItem', @@ -165,11 +167,11 @@ class SongsPlugin(Plugin): """ Rebuild each song. """ - maxSongs = self.manager.get_object_count(Song) - if maxSongs == 0: + max_songs = self.manager.get_object_count(Song) + if max_songs == 0: return progress_dialog = QtGui.QProgressDialog(translate('SongsPlugin', 'Reindexing songs...'), UiStrings().Cancel, - 0, maxSongs, self.main_window) + 0, max_songs, self.main_window) progress_dialog.setWindowTitle(translate('SongsPlugin', 'Reindexing songs')) progress_dialog.setWindowModality(QtCore.Qt.WindowModal) songs = self.manager.get_all_objects(Song) @@ -185,42 +187,61 @@ class SongsPlugin(Plugin): """ DuplicateSongRemovalForm(self).exec_() + def on_import_songselect_item_triggered(self): + """ + Run the SongSelect importer. + """ + self.songselect_form.exec_() + self.media_item.on_search_text_button_clicked() + def on_song_import_item_clicked(self): + """ + Run the song import wizard. + """ if self.media_item: self.media_item.on_import_click() def on_song_export_item_clicked(self): + """ + Run the song export wizard. + """ if self.media_item: self.media_item.on_export_click() def about(self): + """ + Provides information for the plugin manager to display. + + :return: A translatable string with some basic information about the Songs plugin + """ return translate('SongsPlugin', 'Songs Plugin' - '
The songs plugin provides the ability to display and manage songs.') + '
The songs plugin provides the ability to display and manage songs.') def uses_theme(self, theme): """ Called to find out if the song plugin is currently using a theme. - Returns True if the theme is being used, otherwise returns False. + :param theme: The theme to check for usage + :return: True if the theme is being used, otherwise returns False """ if self.manager.get_all_objects(Song, Song.theme_name == theme): return True return False - def rename_theme(self, oldTheme, newTheme): + def rename_theme(self, old_theme, new_theme): """ Renames a theme the song plugin is using making the plugin use the new name. - ``oldTheme`` + ``old_theme`` The name of the theme the plugin should stop using. - ``newTheme`` + ``new_theme`` The new name the plugin should now use. """ - songsUsingTheme = self.manager.get_all_objects(Song, Song.theme_name == oldTheme) - for song in songsUsingTheme: - song.theme_name = newTheme + songs_using_theme = self.manager.get_all_objects(Song, Song.theme_name == old_theme) + for song in songs_using_theme: + song.theme_name = new_theme self.manager.save_object(song) def importSongs(self, format, **kwargs): diff --git a/resources/images/general_back.png b/resources/images/general_back.png new file mode 100644 index 0000000000000000000000000000000000000000..8230340bbb6bfef48e10f8e2079f5cb80b8e66ec GIT binary patch literal 907 zcmV;619bd}P)j;;EmaOqG|jHDZgh1o2e9X_zzv(yH6kdqH_fVDvtwNo z67d+$rElP`DxS9W(wZ1y?HgCmu4$Y*1%h+9+RV+24jvhJ*@BWM#*dA+Y+f~sR+g6_ z?-fBXEbeAeF>kv;s0m`k6^+>d2>KqJme8UJc24<#NfnqGcr+p2n+uqfPukAfGP zMdn@(Jm1GfF~?<45Q_VvtRyD(9=*^3E%HN?wp`q5TcmZx+={ZgaVZ$43BxczX=n;U zhL0$vA`2n3Id~9&F&}MLI`H=HSz>-SMkRncTkWV_jnhlv*aUf9rD;i+rlsL3f)h$Y zcm$PFMqnOr1dPGcQGL!0e9O3DL)!$&S=Zz-!HoJ^8$M$IA&A8QOArmG+C;-CXb6VV zAD|>q^aVZ-xajT6;C;_0XwEmVlmQfH%FX-e`BZ@8l)`o#*tSAEL10-S92zR2Kp><- zz$FSr7Uf>_Jmg)Du#rj(gAHiqUUUwo>Q|f}EU@zaTz-*PWW-|j#-wD zCZ?-5qeu|KgoxHsDfoIXY_6TOkdJnMqUy#yRZ;|Pl~vWvGv_QoDpiJLGJ!-quHhC; z)4-_%&Efolm`M;Tq1Q=_t*n4j^47=h>+1+cQ00Q1klf#`f8R3;m#lOq)-@oR(C|b& zs`}*qX0Xi55zkYMpiFlf{ZFs9xr6=l?{!{&^yFbzG!3SXC9a=MsD;r)@e2*Sxcypk zvF!1~uH)Czr^G;iUjzp+@XwSSzj#S$-O@MBM_SdSru7P==#Q@DLmxj7yC$9O{a_3Y hxzGLVyAJ#}`U0KDUItL)W}5&2002ovPDHLkV1gOknz;Y~ literal 0 HcmV?d00001 diff --git a/resources/images/general_find.png b/resources/images/general_find.png new file mode 100644 index 0000000000000000000000000000000000000000..1b7a252803dd641279df3e3644458dfd6236afea GIT binary patch literal 942 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H3?x5i&EW)6%*9TgAsieWw;%dH0CG7CJR*yM z%CCbkqm#z$3ZS55iEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$QVa}?O9FgC zT>t<74^)o{^z`)f_4UQX#H6I81Ox;y6+lFUgoN1H*#!j!S;2srnHfkkGBU!|17(41 zCMG7JxB?ge)c{3AL_~mw14&U)Q9eFCYinyA9UXHpu(!82G&FQ}b`A*%DJ?Ap8Xg=R z6dDq2V`kLd-RSSjPM}?~qWp4la`Hl4K)s5Jib_gK$^r~3QetXqYPvFly1Kds>WXFxd=?fK zc6N4-j*c!0%U$yCFn9H*sG>^ z7^Ed7rU$uZCMV>WFy`my7lgT$xGR=hFjcsSR#a3}$2!+I^46pV)YsQHH#fJowsv-Q zc6D|2_4Q3iFqtr6!sN-5XC+zAO14^3p4n_7CO&R(@<-PWx;b{;r*{OsAY=Pz8ke&_znS06ro{rdg; zkDot({`&p<&!4}4|Neu5#9LdY08@aLr;B4q#jULqe1i@dFt}Y-$yHq9p&P()s^Rep z>6<%>j6Je_FSw5#w+*J9U$hD`;NseM zwr8a(R@OxN$NqiSD(}y#mh$M!(tK@UCS$jM0`)!|2bav(mKN*26Ro{|Q;}6_%kSb4 zUF&H+Ezj-QSBEYvJMC(`n?FG(V(S5)mg0$LoDXhLj1^SoOuLv-vzh19+00_0zGK_p wPh=~}x@si1{vex^?&HbFf0{ks_A>q>tI>mI%Ql^-pq$Cz>FVdQ&MBb@0Ez8GlK=n! literal 0 HcmV?d00001 diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index 42af83f79..6af0e77a5 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -61,6 +61,8 @@ general_email.png general_revert.png general_clone.png + general_find.png + general_back.png slide_close.png