This commit is contained in:
Raoul Snyman 2010-07-20 06:58:20 +02:00
commit 71ee0eff8c
26 changed files with 789 additions and 518 deletions

View File

@ -654,70 +654,6 @@ class Ui_AmendThemeDialog(object):
QtCore.QObject.connect(self.ThemeButtonBox,
QtCore.SIGNAL(u'rejected()'), AmendThemeDialog.reject)
QtCore.QMetaObject.connectSlotsByName(AmendThemeDialog)
AmendThemeDialog.setTabOrder(self.ThemeButtonBox, self.ThemeNameEdit)
AmendThemeDialog.setTabOrder(self.ThemeNameEdit, self.ThemeTabWidget)
AmendThemeDialog.setTabOrder(self.ThemeTabWidget,
self.BackgroundComboBox)
AmendThemeDialog.setTabOrder(self.BackgroundComboBox,
self.BackgroundTypeComboBox)
AmendThemeDialog.setTabOrder(self.BackgroundTypeComboBox,
self.Color1PushButton)
AmendThemeDialog.setTabOrder(self.Color1PushButton,
self.Color2PushButton)
AmendThemeDialog.setTabOrder(self.Color2PushButton, self.ImageLineEdit)
AmendThemeDialog.setTabOrder(self.ImageLineEdit, self.ImageToolButton)
AmendThemeDialog.setTabOrder(self.ImageToolButton,
self.GradientComboBox)
AmendThemeDialog.setTabOrder(self.GradientComboBox,
self.FontMainComboBox)
AmendThemeDialog.setTabOrder(self.FontMainComboBox,
self.FontMainColorPushButton)
AmendThemeDialog.setTabOrder(self.FontMainColorPushButton,
self.FontMainSizeSpinBox)
AmendThemeDialog.setTabOrder(self.FontMainSizeSpinBox,
self.FontMainWeightComboBox)
AmendThemeDialog.setTabOrder(self.FontMainWeightComboBox,
self.FontMainLineSpacingSpinBox)
AmendThemeDialog.setTabOrder(self.FontMainLineSpacingSpinBox,
self.FontMainDefaultCheckBox)
AmendThemeDialog.setTabOrder(self.FontMainDefaultCheckBox,
self.FontMainXSpinBox)
AmendThemeDialog.setTabOrder(self.FontMainXSpinBox,
self.FontMainYSpinBox)
AmendThemeDialog.setTabOrder(self.FontMainYSpinBox,
self.FontMainWidthSpinBox)
AmendThemeDialog.setTabOrder(self.FontMainWidthSpinBox,
self.FontMainHeightSpinBox)
AmendThemeDialog.setTabOrder(self.FontMainHeightSpinBox,
self.FontFooterComboBox)
AmendThemeDialog.setTabOrder(self.FontFooterComboBox,
self.FontFooterColorPushButton)
AmendThemeDialog.setTabOrder(self.FontFooterColorPushButton,
self.FontFooterSizeSpinBox)
AmendThemeDialog.setTabOrder(self.FontFooterSizeSpinBox,
self.FontFooterWeightComboBox)
AmendThemeDialog.setTabOrder(self.FontFooterWeightComboBox,
self.FontFooterDefaultCheckBox)
AmendThemeDialog.setTabOrder(self.FontFooterDefaultCheckBox,
self.FontFooterXSpinBox)
AmendThemeDialog.setTabOrder(self.FontFooterXSpinBox,
self.FontFooterYSpinBox)
AmendThemeDialog.setTabOrder(self.FontFooterYSpinBox,
self.FontFooterWidthSpinBox)
AmendThemeDialog.setTabOrder(self.FontFooterWidthSpinBox,
self.FontFooterHeightSpinBox)
AmendThemeDialog.setTabOrder(self.FontFooterHeightSpinBox,
self.OutlineCheckBox)
AmendThemeDialog.setTabOrder(self.OutlineCheckBox,
self.OutlineColorPushButton)
AmendThemeDialog.setTabOrder(self.OutlineColorPushButton,
self.ShadowCheckBox)
AmendThemeDialog.setTabOrder(self.ShadowCheckBox,
self.ShadowColorPushButton)
AmendThemeDialog.setTabOrder(self.ShadowColorPushButton,
self.HorizontalComboBox)
AmendThemeDialog.setTabOrder(self.HorizontalComboBox,
self.VerticalComboBox)
def retranslateUi(self, AmendThemeDialog):
AmendThemeDialog.setWindowTitle(

View File

@ -264,6 +264,10 @@ class MainDisplay(DisplayWidget):
(self.screen[u'size'].width() - splash_image.width()) / 2,
(self.screen[u'size'].height() - splash_image.height()) / 2,
splash_image)
#build a blank transparent image
self.transparent = QtGui.QPixmap(
self.screen[u'size'].width(), self.screen[u'size'].height())
self.transparent.fill(QtCore.Qt.transparent)
self.displayImage(self.initialFrame)
self.repaint()
#Build a Black screen
@ -274,12 +278,6 @@ class MainDisplay(DisplayWidget):
QtGui.QImage.Format_ARGB32_Premultiplied)
painter.begin(self.blankFrame)
painter.fillRect(self.blankFrame.rect(), QtCore.Qt.black)
#build a blank transparent image
self.transparent = QtGui.QPixmap(
self.screen[u'size'].width(), self.screen[u'size'].height())
self.transparent.fill(QtCore.Qt.transparent)
# self.displayText.setPixmap(self.transparent)
#self.frameView(self.transparent)
# To display or not to display?
if not self.screen[u'primary']:
self.setVisible(True)
@ -410,6 +408,7 @@ class MainDisplay(DisplayWidget):
self.imageDisplay.setPixmap(QtGui.QPixmap.fromImage(frame))
else:
self.imageDisplay.setPixmap(frame)
self.frameView(self.transparent)
self.videoDisplay.setHtml(u'<html></html>')
def displayVideo(self, path):

View File

@ -118,6 +118,7 @@ class ThemeManager(QtGui.QWidget):
self.checkThemesExists(self.thumbPath)
self.amendThemeForm.path = self.path
self.oldBackgroundImage = None
self.editingDefault = False
# Last little bits of setting up
self.global_theme = unicode(QtCore.QSettings().value(
self.settingsSection + u'/global theme',
@ -184,7 +185,6 @@ class ThemeManager(QtGui.QWidget):
Loads the settings for the theme that is to be edited and launches the
theme editing form so the user can make their changes.
"""
self.editingDefault = False
if check_item_selected(self.ThemeListWidget, translate('ThemeManager',
'You must select a theme to edit.')):
item = self.ThemeListWidget.currentItem()
@ -211,6 +211,14 @@ class ThemeManager(QtGui.QWidget):
'You must select a theme to delete.')):
item = self.ThemeListWidget.currentItem()
theme = unicode(item.text())
# confirm deletion
answer = QtGui.QMessageBox.question(self,
translate('ThemeManager', 'Delete Confirmation'),
translate('ThemeManager', 'Delete theme?'),
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes |
QtGui.QMessageBox.No), QtGui.QMessageBox.No)
if answer == QtGui.QMessageBox.No:
return
# should be the same unless default
if theme != unicode(item.data(QtCore.Qt.UserRole).toString()):
QtGui.QMessageBox.critical(self,
@ -610,6 +618,7 @@ class ThemeManager(QtGui.QWidget):
self.settingsSection + u'/global theme',
QtCore.QVariant(self.global_theme))
Receiver.send_message(u'theme_update_global', self.global_theme)
self.editingDefault = False
self.pushThemes()
else:
# Don't close the dialog - allow the user to change the name of

View File

@ -120,14 +120,6 @@ class Ui_AlertDialog(object):
QtCore.QObject.connect(self.CloseButton, QtCore.SIGNAL(u'clicked()'),
AlertDialog.close)
QtCore.QMetaObject.connectSlotsByName(AlertDialog)
AlertDialog.setTabOrder(self.AlertTextEdit, self.ParameterEdit)
AlertDialog.setTabOrder(self.ParameterEdit, self.AlertListWidget)
AlertDialog.setTabOrder(self.AlertListWidget, self.NewButton)
AlertDialog.setTabOrder(self.NewButton, self.SaveButton)
AlertDialog.setTabOrder(self.SaveButton, self.DeleteButton)
AlertDialog.setTabOrder(self.DeleteButton, self.DisplayButton)
AlertDialog.setTabOrder(self.DisplayButton, self.DisplayCloseButton)
AlertDialog.setTabOrder(self.DisplayCloseButton, self.CloseButton)
def retranslateUi(self, AlertDialog):
AlertDialog.setWindowTitle(

View File

@ -40,7 +40,7 @@ class BiblePlugin(Plugin):
self.weight = -9
self.icon_path = u':/plugins/plugin_bibles.png'
self.icon = build_icon(self.icon_path)
#Register the bible Manager
# Register the bible Manager.
self.status = PluginStatus.Active
self.manager = None
@ -62,7 +62,7 @@ class BiblePlugin(Plugin):
return BiblesTab(self.name)
def getMediaManagerItem(self):
# Create the BibleManagerItem object
# Create the BibleManagerItem object.
return BibleMediaItem(self, self.icon, self.name)
def addImportMenuItem(self, import_menu):
@ -71,7 +71,7 @@ class BiblePlugin(Plugin):
import_menu.addAction(self.ImportBibleItem)
self.ImportBibleItem.setText(
translate('BiblePlugin', '&Bible'))
# Signals and slots
# signals and slots
QtCore.QObject.connect(self.ImportBibleItem,
QtCore.SIGNAL(u'triggered()'), self.onBibleImportClick)
self.ImportBibleItem.setVisible(False)

View File

@ -129,8 +129,7 @@ class BibleMediaItem(MediaManagerItem):
self.QuickClearLabel.setObjectName(u'QuickSearchLabel')
self.QuickLayout.addWidget(self.QuickClearLabel, 4, 0, 1, 1)
self.ClearQuickSearchComboBox = QtGui.QComboBox(self.QuickTab)
self.ClearQuickSearchComboBox.setObjectName(
u'ClearQuickSearchComboBox')
self.ClearQuickSearchComboBox.setObjectName(u'ClearQuickSearchComboBox')
self.QuickLayout.addWidget(self.ClearQuickSearchComboBox, 4, 1, 1, 2)
self.QuickSearchButtonLayout = QtGui.QHBoxLayout()
self.QuickSearchButtonLayout.setMargin(0)
@ -168,8 +167,7 @@ class BibleMediaItem(MediaManagerItem):
self.AdvancedVersionComboBox.setObjectName(u'AdvancedVersionComboBox')
self.AdvancedLayout.addWidget(self.AdvancedVersionComboBox, 0, 1, 1, 2)
self.AdvancedSecondBibleLabel = QtGui.QLabel(self.AdvancedTab)
self.AdvancedSecondBibleLabel.setObjectName(
u'AdvancedSecondBibleLabel')
self.AdvancedSecondBibleLabel.setObjectName(u'AdvancedSecondBibleLabel')
self.AdvancedLayout.addWidget(self.AdvancedSecondBibleLabel, 1, 0, 1, 1)
self.AdvancedSecondBibleComboBox = QtGui.QComboBox(self.AdvancedTab)
self.AdvancedSecondBibleComboBox.setSizeAdjustPolicy(
@ -223,8 +221,7 @@ class BibleMediaItem(MediaManagerItem):
u'AdvancedSearchButtonLayout')
self.AdvancedSearchButtonSpacer = QtGui.QSpacerItem(40, 20,
QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
self.AdvancedSearchButtonLayout.addItem(
self.AdvancedSearchButtonSpacer)
self.AdvancedSearchButtonLayout.addItem(self.AdvancedSearchButtonSpacer)
self.AdvancedSearchButton = QtGui.QPushButton(self.AdvancedTab)
self.AdvancedSearchButton.setObjectName(u'AdvancedSearchButton')
self.AdvancedSearchButtonLayout.addWidget(self.AdvancedSearchButton)
@ -618,8 +615,7 @@ class BibleMediaItem(MediaManagerItem):
else:
self.AdvancedSearchButton.setEnabled(True)
self.AdvancedMessage.setText(u'')
self.adjustComboBox(1, self.chapters_from,
self.AdvancedFromChapter)
self.adjustComboBox(1, self.chapters_from, self.AdvancedFromChapter)
self.adjustComboBox(1, self.chapters_from, self.AdvancedToChapter)
self.adjustComboBox(1, self.verses, self.AdvancedFromVerse)
self.adjustComboBox(1, self.verses, self.AdvancedToVerse)

View File

@ -136,17 +136,6 @@ class Ui_customEditDialog(object):
QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(u'rejected()'),
customEditDialog.closePressed)
QtCore.QMetaObject.connectSlotsByName(customEditDialog)
customEditDialog.setTabOrder(self.TitleEdit, self.VerseTextEdit)
customEditDialog.setTabOrder(self.VerseTextEdit, self.AddButton)
customEditDialog.setTabOrder(self.AddButton, self.VerseListView)
customEditDialog.setTabOrder(self.VerseListView, self.EditButton)
customEditDialog.setTabOrder(self.EditButton, self.EditAllButton)
customEditDialog.setTabOrder(self.EditAllButton, self.SaveButton)
customEditDialog.setTabOrder(self.SaveButton, self.DeleteButton)
customEditDialog.setTabOrder(self.DeleteButton, self.CreditEdit)
customEditDialog.setTabOrder(self.CreditEdit, self.UpButton)
customEditDialog.setTabOrder(self.UpButton, self.DownButton)
customEditDialog.setTabOrder(self.DownButton, self.ThemeComboBox)
def retranslateUi(self, customEditDialog):
customEditDialog.setWindowTitle(

View File

@ -97,7 +97,7 @@ class AuthorsForm(QtGui.QDialog, Ui_AuthorsDialog):
if QtGui.QMessageBox.critical(
self, translate('SongsPlugin.AuthorsForm', 'Error'),
translate('SongsPlugin.AuthorsForm',
'You haven\'t set a display name for the '
'You have not set a display name for the '
'author, would you like me to combine the first and '
'last names for you?'),
QtGui.QMessageBox.StandardButtons(

View File

@ -394,48 +394,12 @@ class Ui_EditSongDialog(object):
QtGui.QDialogButtonBox.Cancel | QtGui.QDialogButtonBox.Save)
self.ButtonBox.setObjectName(u'ButtonBox')
self.verticalLayout.addWidget(self.ButtonBox)
self.retranslateUi(EditSongDialog)
QtCore.QObject.connect(self.ButtonBox,
QtCore.SIGNAL(u'rejected()'), EditSongDialog.closePressed)
QtCore.QObject.connect(self.ButtonBox,
QtCore.SIGNAL(u'accepted()'), EditSongDialog.accept)
QtCore.QMetaObject.connectSlotsByName(EditSongDialog)
EditSongDialog.setTabOrder(self.SongTabWidget, self.TitleEditItem)
EditSongDialog.setTabOrder(self.TitleEditItem, self.AlternativeEdit)
EditSongDialog.setTabOrder(self.AlternativeEdit, self.VerseListWidget)
EditSongDialog.setTabOrder(self.VerseListWidget, self.VerseAddButton)
EditSongDialog.setTabOrder(self.VerseAddButton, self.VerseEditButton)
EditSongDialog.setTabOrder(self.VerseEditButton,
self.VerseEditAllButton)
EditSongDialog.setTabOrder(self.VerseEditAllButton,
self.VerseDeleteButton)
EditSongDialog.setTabOrder(self.VerseDeleteButton, self.VerseOrderEdit)
EditSongDialog.setTabOrder(self.VerseOrderEdit,
self.AuthorsSelectionComboItem)
EditSongDialog.setTabOrder(self.AuthorsSelectionComboItem,
self.AuthorAddButton)
EditSongDialog.setTabOrder(self.AuthorAddButton, self.AuthorsListView)
EditSongDialog.setTabOrder(self.AuthorsListView,
self.AuthorRemoveButton)
EditSongDialog.setTabOrder(self.AuthorRemoveButton,
self.MaintenanceButton)
EditSongDialog.setTabOrder(self.MaintenanceButton, self.SongTopicCombo)
EditSongDialog.setTabOrder(self.SongTopicCombo, self.TopicAddButton)
EditSongDialog.setTabOrder(self.TopicAddButton, self.TopicsListView)
EditSongDialog.setTabOrder(self.TopicsListView, self.TopicRemoveButton)
EditSongDialog.setTabOrder(self.TopicRemoveButton, self.SongbookCombo)
EditSongDialog.setTabOrder(self.SongbookCombo,
self.ThemeSelectionComboItem)
EditSongDialog.setTabOrder(self.ThemeSelectionComboItem,
self.ThemeAddButton)
EditSongDialog.setTabOrder(self.ThemeAddButton, self.CopyrightEditItem)
EditSongDialog.setTabOrder(self.CopyrightEditItem,
self.CopyrightInsertButton)
EditSongDialog.setTabOrder(self.CopyrightInsertButton,
self.CCLNumberEdit)
EditSongDialog.setTabOrder(self.CCLNumberEdit, self.CommentsEdit)
EditSongDialog.setTabOrder(self.CommentsEdit, self.ButtonBox)
def retranslateUi(self, EditSongDialog):
EditSongDialog.setWindowTitle(
@ -466,7 +430,7 @@ class Ui_EditSongDialog(object):
self.AuthorRemoveButton.setText(
translate('SongsPlugin.EditSongForm', '&Remove'))
self.MaintenanceButton.setText(translate('SongsPlugin.EditSongForm',
'&Manage Authors, Topics, Books'))
'&Manage Authors, Topics, Song Books'))
self.TopicGroupBox.setTitle(
translate('SongsPlugin.EditSongForm', 'Topic'))
self.TopicAddButton.setText(
@ -477,7 +441,7 @@ class Ui_EditSongDialog(object):
translate('SongsPlugin.EditSongForm', 'Song Book'))
self.SongTabWidget.setTabText(
self.SongTabWidget.indexOf(self.AuthorsTab),
translate('SongsPlugin.EditSongForm', 'Authors, Topics && Book'))
translate('SongsPlugin.EditSongForm', 'Authors, Topics && Song Book'))
self.ThemeGroupBox.setTitle(
translate('SongsPlugin.EditSongForm', 'Theme'))
self.ThemeAddButton.setText(

View File

@ -68,7 +68,7 @@ class Ui_SongBookDialog(object):
def retranslateUi(self, SongBookDialog):
SongBookDialog.setWindowTitle(
translate('SongsPlugin.SongBookForm', 'Edit Book'))
translate('SongsPlugin.SongBookForm', 'Song Book Maintenance'))
self.NameLabel.setText(translate('SongsPlugin.SongBookForm', '&Name:'))
self.PublisherLabel.setText(
translate('SongsPlugin.SongBookForm', '&Publisher:'))

View File

@ -178,7 +178,8 @@ class ImportWizardForm(QtGui.QWizard, Ui_SongImportWizard):
SettingsManager.get_last_dir(self.songsplugin.settingsSection, 1))
if filename:
editbox.setText(filename)
self.config.set_last_dir(filename, 1)
SettingsManager.set_last_dir(self.songsplugin.settingsSection,
filename, 1)
def incrementProgressBar(self, status_text):
log.debug(u'IncrementBar %s', status_text)
@ -253,4 +254,4 @@ class ImportWizardForm(QtGui.QWizard, Ui_SongImportWizard):
self.ImportProgressBar.setValue(self.ImportProgressBar.maximum())
self.finishButton.setVisible(True)
self.cancelButton.setVisible(False)
Receiver.send_message(u'process_events')
Receiver.send_message(u'process_events')

View File

@ -109,16 +109,16 @@ class Ui_SongImportWizard(object):
self.OpenLyricsButtonLayout.setSpacing(8)
self.OpenLyricsButtonLayout.setObjectName(u'OpenLyricsButtonLayout')
self.OpenLyricsAddButton = QtGui.QPushButton(self.OpenLyricsPage)
self.OpenLyricsAddButton.setIcon(
build_icon(u':/general/general_open.png'))
openIcon = build_icon(u':/general/general_open.png')
self.OpenLyricsAddButton.setIcon(openIcon)
self.OpenLyricsAddButton.setObjectName(u'OpenLyricsAddButton')
self.OpenLyricsButtonLayout.addWidget(self.OpenLyricsAddButton)
self.OpenLyricsButtonSpacer = QtGui.QSpacerItem(40, 20,
QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
self.OpenLyricsButtonLayout.addItem(self.OpenLyricsButtonSpacer)
self.OpenLyricsRemoveButton = QtGui.QPushButton(self.OpenLyricsPage)
self.OpenLyricsRemoveButton.setIcon(
build_icon(u':/general/general_delete.png'))
deleteIcon = build_icon(u':/general/general_delete.png')
self.OpenLyricsRemoveButton.setIcon(deleteIcon)
self.OpenLyricsRemoveButton.setObjectName(u'OpenLyricsRemoveButton')
self.OpenLyricsButtonLayout.addWidget(self.OpenLyricsRemoveButton)
self.OpenLyricsLayout.addLayout(self.OpenLyricsButtonLayout)
@ -136,14 +136,14 @@ class Ui_SongImportWizard(object):
self.OpenSongButtonLayout.setSpacing(8)
self.OpenSongButtonLayout.setObjectName(u'OpenSongButtonLayout')
self.OpenSongAddButton = QtGui.QPushButton(self.OpenSongPage)
self.OpenSongAddButton.setIcon(self.OpenIcon)
self.OpenSongAddButton.setIcon(openIcon)
self.OpenSongAddButton.setObjectName(u'OpenSongAddButton')
self.OpenSongButtonLayout.addWidget(self.OpenSongAddButton)
self.OpenSongButtonSpacer = QtGui.QSpacerItem(40, 20,
QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
self.OpenSongButtonLayout.addItem(self.OpenSongButtonSpacer)
self.OpenSongRemoveButton = QtGui.QPushButton(self.OpenSongPage)
self.OpenSongRemoveButton.setIcon(self.DeleteIcon)
self.OpenSongRemoveButton.setIcon(deleteIcon)
self.OpenSongRemoveButton.setObjectName(u'OpenSongRemoveButton')
self.OpenSongButtonLayout.addWidget(self.OpenSongRemoveButton)
self.OpenSongLayout.addLayout(self.OpenSongButtonLayout)
@ -161,14 +161,14 @@ class Ui_SongImportWizard(object):
self.CCLIButtonLayout.setSpacing(8)
self.CCLIButtonLayout.setObjectName(u'CCLIButtonLayout')
self.CCLIAddButton = QtGui.QPushButton(self.CCLIPage)
self.CCLIAddButton.setIcon(self.OpenIcon)
self.CCLIAddButton.setIcon(openIcon)
self.CCLIAddButton.setObjectName(u'CCLIAddButton')
self.CCLIButtonLayout.addWidget(self.CCLIAddButton)
self.CCLIButtonSpacer = QtGui.QSpacerItem(40, 20,
QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
self.CCLIButtonLayout.addItem(self.CCLIButtonSpacer)
self.CCLIRemoveButton = QtGui.QPushButton(self.CCLIPage)
self.CCLIRemoveButton.setIcon(self.DeleteIcon)
self.CCLIRemoveButton.setIcon(deleteIcon)
self.CCLIRemoveButton.setObjectName(u'CCLIRemoveButton')
self.CCLIButtonLayout.addWidget(self.CCLIRemoveButton)
self.CCLILayout.addLayout(self.CCLIButtonLayout)
@ -190,7 +190,7 @@ class Ui_SongImportWizard(object):
self.CSVFilenameEdit.setObjectName(u'CSVFilenameEdit')
self.CSVFileLayout.addWidget(self.CSVFilenameEdit)
self.CSVBrowseButton = QtGui.QToolButton(self.CSVPage)
self.CSVBrowseButton.setIcon(self.OpenIcon)
self.CSVBrowseButton.setIcon(openIcon)
self.CSVBrowseButton.setObjectName(u'CSVBrowseButton')
self.CSVFileLayout.addWidget(self.CSVBrowseButton)
self.CSVLayout.setLayout(0, QtGui.QFormLayout.FieldRole,
@ -213,14 +213,11 @@ class Ui_SongImportWizard(object):
self.ImportProgressBar.setObjectName(u'ImportProgressBar')
self.ImportLayout.addWidget(self.ImportProgressBar)
SongImportWizard.addPage(self.ImportPage)
self.retranslateUi(SongImportWizard)
self.FormatStackedWidget.setCurrentIndex(0)
QtCore.QObject.connect(
self.FormatComboBox,
QtCore.QObject.connect(self.FormatComboBox,
QtCore.SIGNAL(u'currentIndexChanged(int)'),
self.FormatStackedWidget.setCurrentIndex
)
self.FormatStackedWidget.setCurrentIndex)
QtCore.QMetaObject.connectSlotsByName(SongImportWizard)
def retranslateUi(self, SongImportWizard):
@ -275,4 +272,3 @@ class Ui_SongImportWizard(object):
translate('SongsPlugin.ImportWizardForm', 'Ready.'))
self.ImportProgressBar.setFormat(
translate('SongsPlugin.ImportWizardForm', '%p%'))

View File

@ -217,7 +217,7 @@ class Ui_SongMaintenanceDialog(object):
self.TypeListWidget.item(1).setText(
translate('SongsPlugin.SongMaintenanceForm', 'Topics'))
self.TypeListWidget.item(2).setText(
translate('SongsPlugin.SongMaintenanceForm', 'Books/Hymnals'))
translate('SongsPlugin.SongMaintenanceForm', 'Song Books'))
self.AuthorAddButton.setText(
translate('SongsPlugin.SongMaintenanceForm', '&Add'))
self.AuthorEditButton.setText(

View File

@ -26,9 +26,9 @@
from PyQt4 import QtGui, QtCore
from sqlalchemy.sql import and_
from openlp.core.lib import translate
from openlp.core.lib import Receiver, translate
from openlp.plugins.songs.forms import AuthorsForm, TopicsForm, SongBookForm
from openlp.plugins.songs.lib.db import Author, Book, Topic
from openlp.plugins.songs.lib.db import Author, Book, Topic, Song
from songmaintenancedialog import Ui_SongMaintenanceDialog
class SongMaintenanceForm(QtGui.QDialog, Ui_SongMaintenanceDialog):
@ -137,16 +137,13 @@ class SongMaintenanceForm(QtGui.QDialog, Ui_SongMaintenanceDialog):
def checkAuthor(self, new_author, edit=False):
"""
Returns False when the given Author is already in the list elsewise
Returns False if the given Author is already in the list otherwise
True.
"""
authors = self.songmanager.get_all_objects_filtered(Author,
and_(
Author.first_name == new_author.first_name,
Author.last_name == new_author.last_name,
Author.display_name == new_author.display_name
)
)
authors = self.songmanager.get_all_objects_filtered(Author,
and_(Author.first_name == new_author.first_name,
Author.last_name == new_author.last_name,
Author.display_name == new_author.display_name))
if len(authors) > 0:
# If we edit an existing Author, we need to make sure that we do
# not return False when nothing has changed (because this would
@ -163,11 +160,10 @@ class SongMaintenanceForm(QtGui.QDialog, Ui_SongMaintenanceDialog):
def checkTopic(self, new_topic, edit=False):
"""
Returns False when the given Topic is already in the list elsewise True.
Returns False if the given Topic is already in the list otherwise True.
"""
topics = self.songmanager.get_all_objects_filtered(Topic,
Topic.name == new_topic.name
)
Topic.name == new_topic.name)
if len(topics) > 0:
# If we edit an existing Topic, we need to make sure that we do
# not return False when nothing has changed (because this would
@ -184,25 +180,22 @@ class SongMaintenanceForm(QtGui.QDialog, Ui_SongMaintenanceDialog):
def checkBook(self, new_book, edit=False):
"""
Returns False when the given Book is already in the list elsewise True.
Returns False if the given Book is already in the list otherwise True.
"""
books = self.songmanager.get_all_objects_filtered(Book,
and_(
Book.name == new_book.name,
Book.publisher == new_book.publisher
)
)
and_(Book.name == new_book.name,
Book.publisher == new_book.publisher))
if len(books) > 0:
# If we edit an existing Book, we need to make sure that we do
# not return False when nothing has changed (because this would
# cause an error message later on).
if edit:
if books[0].id == new_book.id:
return True
else:
return False
# If we edit an existing Book, we need to make sure that we do
# not return False when nothing has changed (because this would
# cause an error message later on).
if edit:
if books[0].id == new_book.id:
return True
else:
return False
else:
return False
else:
return True
@ -216,11 +209,16 @@ class SongMaintenanceForm(QtGui.QDialog, Ui_SongMaintenanceDialog):
if self.checkAuthor(author):
if self.songmanager.save_object(author):
self.resetAuthors()
else:
QtGui.QMessageBox.critical(self,
translate('SongsPlugin.SongMaintenanceForm', 'Error'),
translate('SongsPlugin.SongMaintenanceForm',
'Could not add your author.'))
else:
QtGui.QMessageBox.critical(self,
translate('SongsPlugin.SongMaintenanceForm', 'Error'),
translate('SongsPlugin.SongMaintenanceForm',
'Could not add your author.'))
'This author already exists.'))
def onTopicAddButtonClick(self):
if self.topicform.exec_():
@ -228,25 +226,34 @@ class SongMaintenanceForm(QtGui.QDialog, Ui_SongMaintenanceDialog):
if self.checkTopic(topic):
if self.songmanager.save_object(topic):
self.resetTopics()
else:
QtGui.QMessageBox.critical(self,
translate('SongsPlugin.SongMaintenanceForm', 'Error'),
translate('SongsPlugin.SongMaintenanceForm',
'Could not add your topic.'))
else:
QtGui.QMessageBox.critical(self,
translate('SongsPlugin.SongMaintenanceForm', 'Error'),
translate('SongsPlugin.SongMaintenanceForm',
'Could not add your topic.'))
'This topic already exists.'))
def onBookAddButtonClick(self):
if self.bookform.exec_():
book = Book.populate(
name=unicode(self.bookform.NameEdit.text()),
book = Book.populate(name=unicode(self.bookform.NameEdit.text()),
publisher=unicode(self.bookform.PublisherEdit.text()))
if self.checkBook(book):
if self.songmanager.save_object(book):
self.resetBooks()
else:
QtGui.QMessageBox.critical(self,
translate('SongsPlugin.SongMaintenanceForm', 'Error'),
translate('SongsPlugin.SongMaintenanceForm',
'Could not add your book.'))
else:
QtGui.QMessageBox.critical(self,
translate('SongsPlugin.SongMaintenanceForm', 'Error'),
translate('SongsPlugin.SongMaintenanceForm',
'Could not add your book.'))
'This book already exists.'))
def onAuthorEditButtonClick(self):
author_id = self._getCurrentItemId(self.AuthorsListWidget)
@ -270,6 +277,24 @@ class SongMaintenanceForm(QtGui.QDialog, Ui_SongMaintenanceDialog):
if self.checkAuthor(author, True):
if self.songmanager.save_object(author):
self.resetAuthors()
Receiver.send_message(u'songs_load_list')
else:
QtGui.QMessageBox.critical(self,
translate('SongsPlugin.SongMaintenanceForm',
'Error'),
translate('SongsPlugin.SongMaintenanceForm',
'Could not save your changes.'))
elif QtGui.QMessageBox.critical(self,
translate('SongsPlugin.SongMaintenanceForm', 'Error'),
translate('SongsPlugin.SongMaintenanceForm', 'The author %s'
' already exists. Would you like to make songs with author '
'%s use the existing author %s?' % (author.display_name,
temp_display_name, author.display_name)),
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No |
QtGui.QMessageBox.Yes)) == QtGui.QMessageBox.Yes:
self.mergeAuthors(author)
self.resetAuthors()
Receiver.send_message(u'songs_load_list')
else:
# We restore the author's old first and last name as well as
# his display name.
@ -279,7 +304,8 @@ class SongMaintenanceForm(QtGui.QDialog, Ui_SongMaintenanceDialog):
QtGui.QMessageBox.critical(self,
translate('SongsPlugin.SongMaintenanceForm', 'Error'),
translate('SongsPlugin.SongMaintenanceForm',
'Could not save your author.'))
'Could not save your modified author, because he '
'already exists.'))
def onTopicEditButtonClick(self):
topic_id = self._getCurrentItemId(self.TopicsListWidget)
@ -293,13 +319,30 @@ class SongMaintenanceForm(QtGui.QDialog, Ui_SongMaintenanceDialog):
if self.checkTopic(topic, True):
if self.songmanager.save_object(topic):
self.resetTopics()
else:
QtGui.QMessageBox.critical(self,
translate('SongsPlugin.SongMaintenanceForm',
'Error'),
translate('SongsPlugin.SongMaintenanceForm',
'Could not save your changes.'))
elif QtGui.QMessageBox.critical(self,
translate('SongsPlugin.SongMaintenanceForm', 'Error'),
translate('SongsPlugin.SongMaintenanceForm', 'The topic %s '
'already exists. Would you like to make songs with topic %s'
' use the existing topic %s?' % (topic.name, temp_name,
topic.name)),
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No |
QtGui.QMessageBox.Yes)) == QtGui.QMessageBox.Yes:
self.mergeTopics(topic)
self.resetTopics()
else:
# We restore the topics's old name.
topic.name = temp_name
QtGui.QMessageBox.critical(self,
translate('SongsPlugin.SongMaintenanceForm', 'Error'),
translate('SongsPlugin.SongMaintenanceForm',
'Could not save your topic.'))
'Could not save your modified topic, because it '
'already exists.'))
def onBookEditButtonClick(self):
book_id = self._getCurrentItemId(self.BooksListWidget)
@ -317,18 +360,89 @@ class SongMaintenanceForm(QtGui.QDialog, Ui_SongMaintenanceDialog):
if self.checkBook(book, True):
if self.songmanager.save_object(book):
self.resetBooks()
else:
QtGui.QMessageBox.critical(self,
translate('SongsPlugin.SongMaintenanceForm',
'Error'),
translate('SongsPlugin.SongMaintenanceForm',
'Could not save your changes.'))
elif QtGui.QMessageBox.critical(self,
translate('SongsPlugin.SongMaintenanceForm', 'Error'),
translate('SongsPlugin.SongMaintenanceForm', 'The book %s '
'already exists. Would you like to make songs with book %s '
'use the existing book %s?' % (book.name, temp_name,
book.name)),
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.No |
QtGui.QMessageBox.Yes)) == QtGui.QMessageBox.Yes:
self.mergeBooks(book)
self.resetBooks()
else:
# We restore the book's old name and publisher.
book.name = temp_name
book.publisher = temp_publisher
QtGui.QMessageBox.critical(self,
translate('SongsPlugin.SongMaintenanceForm', 'Error'),
translate('SongsPlugin.SongMaintenanceForm',
'Could not save your book.'))
def mergeAuthors(self, old_author):
'''
Merges two authors into one author.
``old_author``
The author which will be deleted afterwards.
'''
existing_author = self.songmanager.get_object_filtered(Author,
and_(Author.first_name == old_author.first_name,
Author.last_name == old_author.last_name,
Author.display_name == old_author.display_name))
songs = self.songmanager.get_all_objects_filtered(Song,
Song.authors.contains(old_author))
for song in songs:
# We check if the song has already existing_author as author. If
# that is not the case we add it.
if existing_author not in song.authors:
song.authors.append(existing_author)
song.authors.remove(old_author)
self.songmanager.save_object(song)
self.songmanager.delete_object(Author, old_author.id)
def mergeTopics(self, old_topic):
'''
Merges two topics into one topic.
``old_topic``
The topic which will be deleted afterwards.
'''
existing_topic = self.songmanager.get_object_filtered(Topic,
Topic.name == old_topic.name)
songs = self.songmanager.get_all_objects_filtered(Song,
Song.topics.contains(old_topic))
for song in songs:
# We check if the song has already existing_topic as topic. If that
# is not the case we add it.
if existing_topic not in song.topics:
song.topics.append(existing_topic)
song.topics.remove(old_topic)
self.songmanager.save_object(song)
self.songmanager.delete_object(Topic, old_topic.id)
def mergeBooks(self, old_book):
'''
Merges two books into one book.
``old_book``
The book which will be deleted afterwards.
'''
existing_book = self.songmanager.get_object_filtered(Book,
and_(Book.name == old_book.name,
Book.publisher == old_book.publisher))
songs = self.songmanager.get_all_objects_filtered(Song,
Song.song_book_id == old_book.id)
for song in songs:
song.song_book_id = existing_book.id
self.songmanager.save_object(song)
self.songmanager.delete_object(Book, old_book.id)
def onAuthorDeleteButtonClick(self):
"""
Delete the author if the author is not attached to any songs
Delete the author if the author is not attached to any songs.
"""
self._deleteItem(Author, self.AuthorsListWidget, self.resetAuthors,
translate('SongsPlugin.SongMaintenanceForm', 'Delete Author'),
@ -341,7 +455,7 @@ class SongMaintenanceForm(QtGui.QDialog, Ui_SongMaintenanceDialog):
def onTopicDeleteButtonClick(self):
"""
Delete the Book is the Book is not attached to any songs
Delete the Book is the Book is not attached to any songs.
"""
self._deleteItem(Topic, self.TopicsListWidget, self.resetTopics,
translate('SongsPlugin.SongMaintenanceForm', 'Delete Topic'),
@ -354,7 +468,7 @@ class SongMaintenanceForm(QtGui.QDialog, Ui_SongMaintenanceDialog):
def onBookDeleteButtonClick(self):
"""
Delete the Book is the Book is not attached to any songs
Delete the Book is the Book is not attached to any songs.
"""
self._deleteItem(Book, self.BooksListWidget, self.resetBooks,
translate('SongsPlugin.SongMaintenanceForm', 'Delete Book'),

View File

@ -141,6 +141,7 @@ from xml import LyricsXML, SongXMLBuilder, SongXMLParser
from songstab import SongsTab
from mediaitem import SongMediaItem
from songimport import SongImport
from opensongimport import OpenSongImport
try:
from sofimport import SofImport
from oooimport import OooImport

View File

@ -0,0 +1,246 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2010 Raoul Snyman #
# Portions copyright (c) 2008-2010 Tim Bentley, Jonathan Corwin, Michael #
# Gorven, Scott Guerrieri, Christian Richter, Maikel Stuivenberg, Martin #
# Thompson, Jon Tibble, Carsten Tinggaard #
# --------------------------------------------------------------------------- #
# 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 #
###############################################################################
import os
import re
from zipfile import ZipFile
from lxml.etree import Element
from lxml import objectify
from openlp.plugins.songs.lib.songimport import SongImport
import logging
log = logging.getLogger(__name__)
class OpenSongImportError(Exception):
pass
class OpenSongImport(object):
"""
Import songs exported from OpenSong - the format is described loosly here:
http://www.opensong.org/d/manual/song_file_format_specification
However, it doesn't describe the <lyrics> section, so here's an attempt:
Verses can be expressed in one of 2 ways:
<lyrics>
[v1]List of words
Another Line
[v2]Some words for the 2nd verse
etc...
</lyrics>
The 'v' can be left out - it is implied
or:
<lyrics>
[V]
1List of words
2Some words for the 2nd Verse
1Another Line
2etc...
</lyrics>
Either or both forms can be used in one song. The Number does not
necessarily appear at the start of the line
The [v1] labels can have either upper or lower case Vs
Other labels can be used also:
C - Chorus
B - Bridge
Guitar chords can be provided 'above' the lyrics (the line is
preceeded by a'.') and _s can be used to signify long-drawn-out
words:
. A7 Bm
1 Some____ Words
Chords and _s are removed by this importer.
The verses etc. are imported and tagged appropriately.
The <presentation> tag is used to populate the OpenLP verse
display order field. The Author and Copyright tags are also
imported to the appropriate places.
"""
def __init__(self, songmanager):
"""
Initialise the class. Requires a songmanager class which
is passed to SongImport for writing song to disk
"""
self.songmanager = songmanager
self.song = None
def do_import(self, filename, commit=True):
"""
Import either a single opensong file, or a zipfile
containing multiple opensong files If the commit parameter is
set False, the import will not be committed to the database
(useful for test scripts)
"""
ext = os.path.splitext(filename)[1]
if ext.lower() == ".zip":
log.info('Zipfile found %s', filename)
z = ZipFile(filename, u'r')
for song in z.infolist():
parts = os.path.split(song.filename)
if parts[-1] == u'':
#No final part => directory
continue
songfile = z.open(song)
self.do_import_file(songfile)
if commit:
self.finish()
else:
log.info('Direct import %s', filename)
file = open(filename)
self.do_import_file(file)
if commit:
self.finish()
def do_import_file(self, file):
"""
Process the OpenSong file - pass in a file-like object,
not a filename
"""
self.song_import = SongImport(self.songmanager)
tree = objectify.parse(file)
root = tree.getroot()
fields = dir(root)
decode = {u'copyright':self.song_import.add_copyright,
u'ccli':u'ccli_number',
u'author':self.song_import.parse_author,
u'title':u'title',
u'aka':u'alternate_title',
u'hymn_number':u'song_number'}
for (attr, fn_or_string) in decode.items():
if attr in fields:
ustring = unicode(root.__getattr__(attr))
if type(fn_or_string) == type(u''):
self.song_import.__setattr__(fn_or_string, ustring)
else:
fn_or_string(ustring)
res = []
if u'theme' in fields:
self.song_import.topics.append(unicode(root.theme))
if u'alttheme' in fields:
self.song_import.topics.append(unicode(root.alttheme))
# data storage while importing
verses = {}
lyrics = unicode(root.lyrics)
# keep track of a "default" verse order, in case none is specified
our_verse_order = []
verses_seen = {}
# in the absence of any other indication, verses are the default,
# erm, versetype!
versetype = u'V'
for thisline in lyrics.split(u'\n'):
# remove comments
semicolon = thisline.find(u';')
if semicolon >= 0:
thisline = thisline[:semicolon]
thisline = thisline.strip()
if len(thisline) == 0:
continue
# skip inthisline guitar chords and page and column breaks
if thisline[0] == u'.' or thisline.startswith(u'---') \
or thisline.startswith(u'-!!'):
continue
# verse/chorus/etc. marker
if thisline[0] == u'[':
versetype = thisline[1].upper()
if versetype.isdigit():
versenum = versetype
versetype = u'V'
elif thisline[2] != u']':
# there's a number to go with it - extract that as well
right_bracket = thisline.find(u']')
versenum = thisline[2:right_bracket]
else:
# if there's no number, assume it's no.1
versenum = u'1'
continue
words = None
# number at start of line.. it's verse number
if thisline[0].isdigit():
versenum = thisline[0]
words = thisline[1:].strip()
if words is None and \
versenum is not None and \
versetype is not None:
words = thisline
if versenum is not None:
versetag = u'%s%s'%(versetype,versenum)
if not verses.has_key(versetype):
verses[versetype] = {}
if not verses[versetype].has_key(versenum):
verses[versetype][versenum] = [] # storage for lines in this verse
if not verses_seen.has_key(versetag):
verses_seen[versetag] = 1
our_verse_order.append(versetag)
if words:
# Tidy text and remove the ____s from extended words
words = self.song_import.tidy_text(words)
words = words.replace('_', '')
verses[versetype][versenum].append(words)
# done parsing
versetypes = verses.keys()
versetypes.sort()
versetags = {}
verse_renames = {}
for versetype in versetypes:
versenums = verses[versetype].keys()
versenums.sort()
for num in versenums:
versetag = u'%s%s' %(versetype,num)
lines = u'\n'.join(verses[versetype][num])
self.song_import.verses.append([versetag, lines])
versetags[versetag] = 1 # keep track of what we have for error checking later
# now figure out the presentation order
if u'presentation' in fields and root.presentation != u'':
order = unicode(root.presentation)
order = order.split()
else:
assert len(our_verse_order)>0
order = our_verse_order
for tag in order:
if len(tag) == 1:
tag = tag + u'1' # Assume it's no.1 if it's not there
if not versetags.has_key(tag):
log.warn(u'Got order %s but not in versetags, skipping', tag)
else:
self.song_import.verse_order_list.append(tag)
def finish(self):
""" Separate function, allows test suite to not pollute database"""
self.song_import.finish()

View File

@ -142,7 +142,7 @@ class SofImport(OooImport):
self.blanklines += 1
if self.blanklines > 1:
return
if self.song.get_title() != u'':
if self.song.title != u'':
self.finish_verse()
return
self.blanklines = 0
@ -166,8 +166,8 @@ class SofImport(OooImport):
self.finish_verse()
self.song.repeat_verse()
return
if self.song.get_title() == u'':
if self.song.get_copyright() == u'':
if self.song.title == u'':
if self.song.copyright == u'':
self.add_author(text)
else:
self.song.add_copyright(text)
@ -187,10 +187,10 @@ class SofImport(OooImport):
return text
if textportion.CharWeight == BOLD:
boldtext = text.strip()
if boldtext.isdigit() and self.song.get_song_number() == '':
if boldtext.isdigit() and self.song.song_number == '':
self.add_songnumber(boldtext)
return u''
if self.song.get_title() == u'':
if self.song.title == u'':
text = self.uncap_text(text)
self.add_title(text)
return text
@ -220,20 +220,17 @@ class SofImport(OooImport):
Add a song number, store as alternate title. Also use the song
number to work out which songbook we're in
"""
self.song.set_song_number(song_no)
self.song.set_alternate_title(song_no + u'.')
self.song.song_number = song_no
self.song.alternate_title = song_no + u'.'
self.song.song_book_pub = u'Kingsway Publications'
if int(song_no) <= 640:
self.song.set_song_book(u'Songs of Fellowship 1',
u'Kingsway Publications')
self.song.song_book = u'Songs of Fellowship 1'
elif int(song_no) <= 1150:
self.song.set_song_book(u'Songs of Fellowship 2',
u'Kingsway Publications')
self.song.song_book = u'Songs of Fellowship 2'
elif int(song_no) <= 1690:
self.song.set_song_book(u'Songs of Fellowship 3',
u'Kingsway Publications')
self.song.song_book = u'Songs of Fellowship 3'
else:
self.song.set_song_book(u'Songs of Fellowship 4',
u'Kingsway Publications')
self.song.song_book = u'Songs of Fellowship 4'
def add_title(self, text):
"""
@ -245,7 +242,7 @@ class SofImport(OooImport):
title = title[1:]
if title.endswith(u','):
title = title[:-1]
self.song.set_title(title)
self.song.title = title
def add_author(self, text):
"""
@ -283,7 +280,7 @@ class SofImport(OooImport):
splitat = None
else:
versetag = u'V'
splitat = self.verse_splits(self.song.get_song_number())
splitat = self.verse_splits(self.song.song_number)
if splitat:
ln = 0
verse = u''
@ -538,4 +535,3 @@ class SofImport(OooImport):
if song_number == 1119:
return 7
return None

View File

@ -123,53 +123,10 @@ class SongImport(object):
if len(lines) == 1:
self.parse_author(lines[0])
return
if not self.get_title():
self.set_title(lines[0])
if not self.title:
self.title = lines[0]
self.add_verse(text)
def get_title(self):
"""
Return the title
"""
return self.title
def get_copyright(self):
"""
Return the copyright
"""
return self.copyright
def get_song_number(self):
"""
Return the song number
"""
return self.song_number
def set_title(self, title):
"""
Set the title
"""
self.title = title
def set_alternate_title(self, title):
"""
Set the alternate title
"""
self.alternate_title = title
def set_song_number(self, song_number):
"""
Set the song number
"""
self.song_number = song_number
def set_song_book(self, song_book, publisher):
"""
Set the song book name and publisher
"""
self.song_book_name = song_book
self.song_book_pub = publisher
def add_copyright(self, copyright):
"""
Build the copyright field
@ -184,8 +141,8 @@ class SongImport(object):
"""
Add the author. OpenLP stores them individually so split by 'and', '&'
and comma.
However need to check for "Mr and Mrs Smith" and turn it to
"Mr Smith" and "Mrs Smith".
However need to check for 'Mr and Mrs Smith' and turn it to
'Mr Smith' and 'Mrs Smith'.
"""
for author in text.split(u','):
authors = author.split(u'&')
@ -267,7 +224,7 @@ class SongImport(object):
def commit_song(self):
"""
Write the song and it's fields to disk
Write the song and its fields to disk
"""
song = Song()
song.title = self.title
@ -303,29 +260,24 @@ class SongImport(object):
author = self.manager.get_object_filtered(Author,
Author.display_name == authortext)
if author is None:
author = Author()
author.display_name = authortext
author.last_name = authortext.split(u' ')[-1]
author.first_name = u' '.join(authortext.split(u' ')[:-1])
self.manager.save_object(author)
author = Author.populate(display_name = authortext,
last_name=authortext.split(u' ')[-1],
first_name=u' '.join(authortext.split(u' ')[:-1]))
song.authors.append(author)
if self.song_book_name:
song_book = self.manager.get_object_filtered(Book,
Book.name == self.song_book_name)
if song_book is None:
song_book = Book()
song_book.name = self.song_book_name
song_book.publisher = self.song_book_pub
self.manager.save_object(song_book)
song.song_book_id = song_book.id
song_book = Book.populate(name=self.song_book_name,
publisher=self.song_book_pub)
song.book = song_book
for topictext in self.topics:
topic = self.manager.get_object_filtered(Topic,
Topic.name == topictext)
if len(topictext) == 0:
continue
topic = self.manager.get_object_filtered(Topic, Topic.name == topictext)
if topic is None:
topic = Topic()
topic.name = topictext
self.manager.save_object(topic)
song.topics.append(topictext)
topic = Topic.populate(name=topictext)
song.topics.append(topic)
self.manager.save_object(song)
def print_song(self):

View File

@ -60,175 +60,6 @@ class SongFeatureError(SongException):
# TODO: Song: Import ChangingSong
# TODO: Song: Export ChangingSong
_BLANK_OPENSONG_XML = \
'''<?xml version="1.0" encoding="UTF-8"?>
<song>
<title></title>
<author></author>
<copyright></copyright>
<presentation></presentation>
<ccli></ccli>
<lyrics></lyrics>
<theme></theme>
<alttheme></alttheme>
</song>
'''
class _OpenSong(object):
"""
Class for import of OpenSong
"""
def __init__(self, xmlContent = None):
"""
Initialize from given xml content
"""
self._set_from_xml(_BLANK_OPENSONG_XML, 'song')
if xmlContent:
self._set_from_xml(xmlContent, 'song')
def _set_from_xml(self, xml, root_tag):
"""
Set song properties from given xml content.
``xml``
Formatted xml tags and values.
``root_tag``
The root tag of the xml.
"""
root = ElementTree(element=XML(xml))
xml_iter = root.getiterator()
for element in xml_iter:
if element.tag != root_tag:
text = element.text
if text is None:
val = text
elif isinstance(text, basestring):
# Strings need special handling to sort the colours out
if text[0] == u'$':
# This might be a hex number, let's try to convert it.
try:
val = int(text[1:], 16)
except ValueError:
pass
else:
# Let's just see if it's a integer.
try:
val = int(text)
except ValueError:
# Ok, it seems to be a string.
val = text
if hasattr(self, u'post_tag_hook'):
(element.tag, val) = \
self.post_tag_hook(element.tag, val)
setattr(self, element.tag, val)
def __str__(self):
"""
Return string with all public attributes
The string is formatted with one attribute per line
If the string is split on newline then the length of the
list is equal to the number of attributes
"""
attributes = []
for attrib in dir(self):
if not attrib.startswith(u'_'):
attributes.append(
u'%30s : %s' % (attrib, getattr(self, attrib)))
return u'\n'.join(attributes)
def _get_as_string(self):
"""
Return one string with all public attributes
"""
result = u''
for attrib in dir(self):
if not attrib.startswith(u'_'):
result += u'_%s_' % getattr(self, attrib)
return result
def get_author_list(self):
"""Convert author field to an authorlist
in OpenSong an author list may be separated by '/'
return as a string
"""
if self.author:
list = self.author.split(u' and ')
res = [item.strip() for item in list]
return u', '.join(res)
def get_category_array(self):
"""Convert theme and alttheme into category_array
return as a string
"""
res = []
if self.theme:
res.append(self.theme)
if self.alttheme:
res.append(self.alttheme)
return u', u'.join(res)
def _reorder_verse(self, tag, tmpVerse):
"""
Reorder the verse in case of first char is a number
tag -- the tag of this verse / verse group
tmpVerse -- list of strings
"""
res = []
for digit in '1234567890 ':
tagPending = True
for line in tmpVerse:
if line.startswith(digit):
if tagPending:
tagPending = False
tagChar = tag.strip(u'[]').lower()
if 'v' == tagChar:
newtag = "Verse"
elif 'c' == tagChar:
newtag = "Chorus"
elif 'b' == tagChar:
newtag = "Bridge"
elif 'p' == tagChar:
newtag = "Pre-chorus"
else:
newtag = tagChar
tagString = (u'# %s %s' % (newtag, digit)).rstrip()
res.append(tagString)
res.append(line[1:])
if (len(line) == 0) and (not tagPending):
res.append(line)
return res
def get_lyrics(self):
"""
Convert the lyrics to openlp lyrics format
return as list of strings
"""
lyrics = self.lyrics.split(u'\n')
tmpVerse = []
finalLyrics = []
tag = ""
for lyric in lyrics:
line = lyric.rstrip()
if not line.startswith(u'.'):
# drop all chords
tmpVerse.append(line)
if line:
if line.startswith(u'['):
tag = line
else:
reorderedVerse = self._reorder_verse(tag, tmpVerse)
finalLyrics.extend(reorderedVerse)
tag = ""
tmpVerse = []
# catch up final verse
reorderedVerse = self._reorder_verse(tag, tmpVerse)
finalLyrics.extend(reorderedVerse)
return finalLyrics
class Song(object):
"""Handling song properties and methods
@ -275,7 +106,7 @@ class Song(object):
show_author_list -- 0: no show, 1: show
show_copyright -- 0: no show, 1: show
show_song_cclino -- 0: no show, 1: show
theme -- name of theme or blank
theme_name -- name of theme or blank
category_array -- list of user defined properties (hymn, gospel)
song_book -- name of originating book
song_number -- number of the song, related to a songbook
@ -298,7 +129,7 @@ class Song(object):
self.show_copyright = 1
self.show_song_cclino = 1
self.show_title = 1
self.theme = ""
self.theme_name = ""
self.category_array = None
self.song_book = ""
self.song_number = ""
@ -307,40 +138,6 @@ class Song(object):
self.set_lyrics(u'')
return
def from_opensong_buffer(self, xmlcontent):
"""Initialize from buffer(string) of xml lines in opensong format"""
self._reset()
opensong = _OpenSong(xmlcontent)
if opensong.title:
self.set_title(opensong.title)
if opensong.copyright:
self.set_copyright(opensong.copyright)
if opensong.presentation:
self.set_verse_order(opensong.presentation)
if opensong.ccli:
self.set_song_cclino(opensong.ccli)
self.set_author_list(opensong.get_author_list())
self.set_category_array(opensong.get_category_array())
self.set_lyrics(opensong.get_lyrics())
def from_opensong_file(self, xmlfilename):
"""
Initialize from file containing xml
xmlfilename -- path to xml file
"""
osfile = None
try:
osfile = open(xmlfilename, 'r')
list = [line for line in osfile]
osfile.close()
xml = "".join(list)
self.from_opensong_buffer(xml)
except IOError:
log.exception(u'Failed to load opensong xml file')
finally:
if osfile:
osfile.close()
def _remove_punctuation(self, title):
"""Remove the puntuation chars from title
@ -366,10 +163,6 @@ class Song(object):
if len(self.search_title) < 1:
raise SongTitleError(u'The searchable title is empty')
def get_title(self):
"""Return title value"""
return self.title
def from_ccli_text_buffer(self, textList):
"""
Create song from a list of texts (strings) - CCLI text format expected
@ -424,7 +217,7 @@ class Song(object):
self.set_title(sName)
self.set_author_list(author_list)
self.set_copyright(sCopyright)
self.set_song_cclino(sCcli)
self.set_ccli_number(sCcli)
self.set_lyrics(lyrics)
def from_ccli_text_file(self, textFileName):
@ -479,21 +272,21 @@ class Song(object):
"""Set the copyright string"""
self.copyright = copyright
def get_song_cclino(self):
def get_ccli_number(self):
"""Return the songCclino"""
return self._assure_string(self.song_cclino)
return self._assure_string(self.ccli_number)
def set_song_cclino(self, song_cclino):
"""Set the song_cclino"""
self.song_cclino = song_cclino
def set_ccli_number(self, ccli_number):
"""Set the ccli_number"""
self.ccli_number = ccli_number
def get_theme(self):
def get_theme_name(self):
"""Return the theme name for the song"""
return self._assure_string(self.theme)
return self._assure_string(self.theme_name)
def set_theme(self, theme):
def set_theme_name(self, theme_name):
"""Set the theme name (string)"""
self.theme = theme
self.theme_name = theme_name
def get_song_book(self):
"""Return the song_book (string)"""
@ -532,9 +325,9 @@ class Song(object):
asOneString
True -- string:
"John Newton, A Parker"
'John Newton, A Parker'
False -- list of strings
["John Newton", u'A Parker"]
['John Newton', u'A Parker']
"""
if asOneString:
res = self._assure_string(self.author_list)
@ -557,9 +350,9 @@ class Song(object):
asOneString
True -- string:
"Hymn, Gospel"
'Hymn, Gospel'
False -- list of strings
["Hymn", u'Gospel"]
['Hymn', u'Gospel']
"""
if asOneString:
res = self._assure_string(self.category_array)
@ -601,13 +394,13 @@ class Song(object):
"""Set the show_copyright flag (bool)"""
self.show_copyright = show_copyright
def get_show_song_cclino(self):
def get_show_ccli_number(self):
"""Return the showSongCclino (string)"""
return self.show_song_cclino
return self.show_ccli_number
def set_show_song_cclino(self, show_song_cclino):
"""Set the show_song_cclino flag (bool)"""
self.show_song_cclino = show_song_cclino
def set_show_ccli_number(self, show_ccli_number):
"""Set the show_ccli_number flag (bool)"""
self.show_ccli_number = show_ccli_number
def get_lyrics(self):
"""Return the lyrics as a list of strings
@ -674,7 +467,7 @@ class Song(object):
slideNumber -- 1 .. numberOfSlides
Returns a list as:
[theme (string),
[theme_name (string),
title (string),
authorlist (string),
copyright (string),
@ -688,7 +481,7 @@ class Song(object):
raise SongSlideError(u'Slide number too high')
res = []
if self.show_title:
title = self.get_title()
title = self.title
else:
title = ""
if self.show_author_list:
@ -699,13 +492,13 @@ class Song(object):
cpright = self.get_copyright()
else:
cpright = ""
if self.show_song_cclino:
ccli = self.get_song_cclino()
if self.show_ccli_number:
ccli = self.get_ccli_number()
else:
ccli = ""
theme = self.get_theme()
theme_name = self.get_theme_name()
# examine the slide for a theme
res.append(theme)
res.append(theme_name)
res.append(title)
res.append(author)
res.append(cpright)

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<song>
<title>Martins Test</title>
<author>MartiÑ Thómpson</author>
<copyright>2010 Martin Thompson</copyright>
<hymn_number>1</hymn_number>
<presentation>V1 C V2 C2 V3 B1 V1</presentation>
<ccli>Blah</ccli>
<capo print="false"></capo>
<key></key>
<aka></aka>
<key_line></key_line>
<user1></user1>
<user2></user2>
<user3></user3>
<theme>TestTheme</theme>
<alttheme>TestAltTheme</alttheme>
<tempo></tempo>
<time_sig></time_sig>
<lyrics>;Comment
. A B C
1 v1 Line 1___
2 v2 Line 1___
. A B C7
1 V1 Line 2
2 V2 Line 2
[3]
V3 Line 1
V3 Line 2
[b1]
Bridge 1
---
-!!
Bridge 1 line 2
[C]
. A B
Chorus 1
[C2]
. A B
Chorus 2
</lyrics>
<style index="default_style">
<title enabled="true" valign="bottom" align="center" include_verse="false" margin-left="0" margin-right="0" margin-top="0" margin-bottom="0" font="Helvetica" size="26" bold="true" italic="true" underline="false" color="#FFFFFF" border="true" border_color="#000000" shadow="true" shadow_color="#000000" fill="false" fill_color="#000000"/>
<subtitle enabled="true" valign="bottom" align="center" descriptive="false" margin-left="0" margin-right="0" margin-top="0" margin-bottom="0" font="Helvetica" size="18" bold="true" italic="true" underline="false" color="#FFFFFF" border="true" border_color="#000000" shadow="true" shadow_color="#000000" fill="false" fill_color="#000000"/>
<song_subtitle>author</song_subtitle>
<body enabled="true" auto_scale="false" valign="middle" align="center" highlight_chorus="true" margin-left="0" margin-right="0" margin-top="0" margin-bottom="0" font="Helvetica" size="34" bold="true" italic="false" underline="false" color="#FFFFFF" border="true" border_color="#000000" shadow="true" shadow_color="#000000" fill="false" fill_color="#FF0000">
<tabs/>
</body>
<background strip_footer="0" color="#408080" position="1"/>
</style></song>

Binary file not shown.

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<song>
<title>Martins 2nd Test</title>
<author>Martin Thompson</author>
<copyright>2010 Martin Thompson</copyright>
<hymn_number>2</hymn_number>
<presentation></presentation>
<ccli>Blah</ccli>
<capo print="false"></capo>
<key></key>
<aka></aka>
<key_line></key_line>
<user1></user1>
<user2></user2>
<user3></user3>
<theme></theme>
<tempo></tempo>
<time_sig></time_sig>
<lyrics>;Comment
[V]
. A B C
1 v1 Line 1___
2 v2 Line 1___
. A B C7
1 V1 Line 2
2 V2 Line 2
[b1]
Bridge 1
Bridge 1 line 2
[C1]
Chorus 1
[C2]
Chorus 2
</lyrics>
<style index="default_style">
<title enabled="true" valign="bottom" align="center" include_verse="false" margin-left="0" margin-right="0" margin-top="0" margin-bottom="0" font="Helvetica" size="26" bold="true" italic="true" underline="false" color="#FFFFFF" border="true" border_color="#000000" shadow="true" shadow_color="#000000" fill="false" fill_color="#000000"/>
<subtitle enabled="true" valign="bottom" align="center" descriptive="false" margin-left="0" margin-right="0" margin-top="0" margin-bottom="0" font="Helvetica" size="18" bold="true" italic="true" underline="false" color="#FFFFFF" border="true" border_color="#000000" shadow="true" shadow_color="#000000" fill="false" fill_color="#000000"/>
<song_subtitle>author</song_subtitle>
<body enabled="true" auto_scale="false" valign="middle" align="center" highlight_chorus="true" margin-left="0" margin-right="0" margin-top="0" margin-bottom="0" font="Helvetica" size="34" bold="true" italic="false" underline="false" color="#FFFFFF" border="true" border_color="#000000" shadow="true" shadow_color="#000000" fill="false" fill_color="#FF0000">
<tabs/>
</body>
<background strip_footer="0" color="#408080" position="1"/>
</style></song>

View File

@ -0,0 +1,57 @@
from openlp.plugins.songs.lib.opensongimport import OpenSongImport
from openlp.plugins.songs.lib.db import init_schema
from openlp.core.lib.db import Manager
from glob import glob
from zipfile import ZipFile
import os
from traceback import print_exc
import sys
import codecs
def opensong_import_lots():
ziploc = u'/home/mjt/openlp/OpenSong_Data/'
files = []
#files = [u'test.opensong.zip', ziploc+u'ADond.zip']
files.extend(glob(ziploc+u'Songs.zip'))
#files.extend(glob(ziploc+u'SOF.zip'))
#files.extend(glob(ziploc+u'spanish_songs_for_opensong.zip'))
# files.extend(glob(ziploc+u'opensong_*.zip'))
errfile = codecs.open(u'import_lots_errors.txt', u'w', u'utf8')
manager = Manager(u'songs', init_schema)
for file in files:
print u'Importing', file
z = ZipFile(file, u'r')
for song in z.infolist():
# need to handle unicode filenames (CP437 - Winzip does this)
filename = song.filename#.decode('cp852')
parts = os.path.split(filename)
if parts[-1] == u'':
#No final part => directory
continue
print " ", file, ":",filename,
songfile = z.open(song)
#z.extract(song)
#songfile=open(filename, u'r')
o = OpenSongImport(manager)
try:
o.do_import_file(songfile)
# o.song_import.print_song()
except:
print "Failure",
errfile.write(u'Failure: %s:%s\n' %(file, filename.decode('cp437')))
songfile = z.open(song)
for l in songfile.readlines():
l = l.decode('utf8')
print(u' |%s\n' % l.strip())
errfile.write(u' |%s\n'%l.strip())
print_exc(3, file = errfile)
print_exc(3)
sys.exit(1)
# continue
#o.finish()
print "OK"
#os.unlink(filename)
# o.song_import.print_song()
if __name__ == "__main__":
opensong_import_lots()

View File

@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2010 Raoul Snyman #
# Portions copyright (c) 2008-2010 Tim Bentley, Jonathan Corwin, Michael #
# Gorven, Scott Guerrieri, Christian Richter, Maikel Stuivenberg, Martin #
# Thompson, Jon Tibble, Carsten Tinggaard #
# --------------------------------------------------------------------------- #
# 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 #
###############################################################################
from openlp.plugins.songs.lib.opensongimport import OpenSongImport
from openlp.core.lib.db import Manager
from openlp.plugins.songs.lib.db import init_schema
from openlp.plugins.songs.songsplugin import SongsPlugin
import sys
def test():
manager = Manager(u'songs', init_schema)
o = OpenSongImport(manager)
o.do_import(u'test.opensong', commit=False)
o.song_import.print_song()
assert o.song_import.copyright == u'2010 Martin Thompson'
assert o.song_import.authors == [u'MartiÑ Thómpson']
assert o.song_import.title == u'Martins Test'
assert o.song_import.alternate_title == u''
assert o.song_import.song_number == u'1'
assert [u'C1', u'Chorus 1'] in o.song_import.verses
assert [u'C2', u'Chorus 2'] in o.song_import.verses
assert not [u'C3', u'Chorus 3'] in o.song_import.verses
assert [u'B1', u'Bridge 1\nBridge 1 line 2'] in o.song_import.verses
assert [u'V1', u'v1 Line 1\nV1 Line 2'] in o.song_import.verses
assert [u'V2', u'v2 Line 1\nV2 Line 2'] in o.song_import.verses
assert o.song_import.verse_order_list == [u'V1', u'C1', u'V2', u'C2', u'V3', u'B1', u'V1']
assert o.song_import.ccli_number == u'Blah'
assert o.song_import.topics == [u'TestTheme', u'TestAltTheme']
o.do_import(u'test.opensong.zip', commit=False)
o.song_import.print_song()
o.finish()
assert o.song_import.copyright == u'2010 Martin Thompson'
assert o.song_import.authors == [u'MartiÑ Thómpson']
assert o.song_import.title == u'Martins Test'
assert o.song_import.alternate_title == u''
assert o.song_import.song_number == u'1'
assert [u'B1', u'Bridge 1\nBridge 1 line 2'] in o.song_import.verses
assert [u'C1', u'Chorus 1'] in o.song_import.verses
assert [u'C2', u'Chorus 2'] in o.song_import.verses
assert not [u'C3', u'Chorus 3'] in o.song_import.verses
assert [u'V1', u'v1 Line 1\nV1 Line 2'] in o.song_import.verses
assert [u'V2', u'v2 Line 1\nV2 Line 2'] in o.song_import.verses
assert o.song_import.verse_order_list == [u'V1', u'C1', u'V2', u'C2', u'V3', u'B1', u'V1']
o = OpenSongImport(manager)
o.do_import(u'test2.opensong', commit=False)
# o.finish()
o.song_import.print_song()
assert o.song_import.copyright == u'2010 Martin Thompson'
assert o.song_import.authors == [u'Martin Thompson']
assert o.song_import.title == u'Martins 2nd Test'
assert o.song_import.alternate_title == u''
assert o.song_import.song_number == u'2'
print o.song_import.verses
assert [u'B1', u'Bridge 1\nBridge 1 line 2'] in o.song_import.verses
assert [u'C1', u'Chorus 1'] in o.song_import.verses
assert [u'C2', u'Chorus 2'] in o.song_import.verses
assert not [u'C3', u'Chorus 3'] in o.song_import.verses
assert [u'V1', u'v1 Line 1\nV1 Line 2'] in o.song_import.verses
assert [u'V2', u'v2 Line 1\nV2 Line 2'] in o.song_import.verses
print o.song_import.verse_order_list
assert o.song_import.verse_order_list == [u'V1', u'V2', u'B1', u'C1', u'C2']
print "Tests passed"
pass
if __name__ == "__main__":
test()

View File

@ -76,8 +76,9 @@ class SongXMLBuilder(object):
``content``
The actual text of the verse to be stored.
"""
#log.debug(u'add_verse_to_lyrics %s, %s\n%s' % (type, number, content))
verse = etree.Element(u'verse', type=type, label=number)
# log.debug(u'add_verse_to_lyrics %s, %s\n%s' % (type, number, content))
verse = etree.Element(u'verse', type = unicode(type),
label = unicode(number))
verse.text = etree.CDATA(content)
self.lyrics.append(verse)

View File

@ -39,6 +39,8 @@ try:
except ImportError:
OOo_available = False
from openlp.plugins.songs.lib import OpenSongImport
log = logging.getLogger(__name__)
class SongsPlugin(Plugin):
@ -137,6 +139,25 @@ class SongsPlugin(Plugin):
QtCore.SIGNAL(u'triggered()'), self.onImportSofItemClick)
QtCore.QObject.connect(self.ImportOooItem,
QtCore.SIGNAL(u'triggered()'), self.onImportOooItemClick)
# OpenSong import menu item - will be removed and the
# functionality will be contained within the import wizard
self.ImportOpenSongItem = QtGui.QAction(import_menu)
self.ImportOpenSongItem.setObjectName(u'ImportOpenSongItem')
self.ImportOpenSongItem.setText(
translate('SongsPlugin',
'OpenSong (temp menu item)'))
self.ImportOpenSongItem.setToolTip(
translate('SongsPlugin',
'Import songs from OpenSong files' +
'(either raw text or ZIPfiles)'))
self.ImportOpenSongItem.setStatusTip(
translate('SongsPlugin',
'Import songs from OpenSong files' +
'(either raw text or ZIPfiles)'))
import_menu.addAction(self.ImportOpenSongItem)
QtCore.QObject.connect(self.ImportOpenSongItem,
QtCore.SIGNAL(u'triggered()'), self.onImportOpenSongItemClick)
def addExportMenuItem(self, export_menu):
"""
@ -177,6 +198,26 @@ class SongsPlugin(Plugin):
QtGui.QMessageBox.Ok)
Receiver.send_message(u'songs_load_list')
def onImportOpenSongItemClick(self):
filenames = QtGui.QFileDialog.getOpenFileNames(
None, translate('SongsPlugin',
'Open OpenSong file'),
u'', u'All files (*.*)')
try:
for filename in filenames:
importer = OpenSongImport(self.manager)
importer.do_import(unicode(filename))
except:
log.exception('Could not import OpenSong file')
QtGui.QMessageBox.critical(None,
translate('SongsPlugin',
'Import Error'),
translate('SongsPlugin',
'Error importing OpenSong file'),
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok),
QtGui.QMessageBox.Ok)
Receiver.send_message(u'songs_load_list')
def onImportOooItemClick(self):
filenames = QtGui.QFileDialog.getOpenFileNames(
None, translate('SongsPlugin',