diff --git a/openlp/core/lib/formattingtags.py b/openlp/core/lib/formattingtags.py index 01d679a17..f914677c6 100644 --- a/openlp/core/lib/formattingtags.py +++ b/openlp/core/lib/formattingtags.py @@ -36,7 +36,7 @@ from openlp.core.lib import Settings, translate class FormattingTags(object): """ - Static Class to HTML Tags to be access around the code the list is managed by the Options Tab. + Static Class for HTML Tags to be access around the code the list is managed by the Options Tab. """ html_expands = [] @@ -48,22 +48,15 @@ class FormattingTags(object): return FormattingTags.html_expands @staticmethod - def save_html_tags(): + def save_html_tags(new_tags): """ - Saves all formatting tags except protected ones. + Saves all formatting tags except protected ones + + `new_tags` + The tags to be saved.. """ - tags = [] - for tag in FormattingTags.html_expands: - if not tag['protected'] and not tag.get('temporary'): - # Using dict ensures that copy is made and encoding of values a little later does not affect tags in - # the original list - tags.append(dict(tag)) - tag = tags[-1] - # Remove key 'temporary' from tags. It is not needed to be saved. - if 'temporary' in tag: - del tag['temporary'] # Formatting Tags were also known as display tags. - Settings().setValue('formattingTags/html_tags', json.dumps(tags) if tags else '') + Settings().setValue('formattingTags/html_tags', json.dumps(new_tags) if new_tags else '') @staticmethod def load_tags(): diff --git a/openlp/core/ui/__init__.py b/openlp/core/ui/__init__.py index 14ac2fc47..410a8fc16 100644 --- a/openlp/core/ui/__init__.py +++ b/openlp/core/ui/__init__.py @@ -95,6 +95,7 @@ from .aboutform import AboutForm from .pluginform import PluginForm from .settingsform import SettingsForm from .formattingtagform import FormattingTagForm +from .formattingtagcontroller import FormattingTagController from .shortcutlistform import ShortcutListForm from .mediadockmanager import MediaDockManager from .servicemanager import ServiceManager @@ -104,4 +105,4 @@ __all__ = ['SplashScreen', 'AboutForm', 'SettingsForm', 'MainDisplay', 'SlideCon 'ThemeManager', 'MediaDockManager', 'ServiceItemEditForm', 'FirstTimeForm', 'FirstTimeLanguageForm', 'ThemeForm', 'ThemeLayoutForm', 'FileRenameForm', 'StartTimeForm', 'MainDisplay', 'Display', 'ServiceNoteForm', 'SlideController', 'DisplayController', 'GeneralTab', 'ThemesTab', 'AdvancedTab', 'PluginForm', - 'FormattingTagForm', 'ShortcutListForm'] + 'FormattingTagForm', 'ShortcutListForm', 'FormattingTagController'] diff --git a/openlp/core/ui/formattingtagcontroller.py b/openlp/core/ui/formattingtagcontroller.py new file mode 100644 index 000000000..9b891849b --- /dev/null +++ b/openlp/core/ui/formattingtagcontroller.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2013 Raoul Snyman # +# Portions copyright (c) 2008-2013 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:`formattingtagform` provides an Tag Edit facility. The Base set are protected and included each time loaded. +Custom tags can be defined and saved. The Custom Tag arrays are saved in a pickle so QSettings works on them. Base Tags +cannot be changed. +""" + +import re + +from openlp.core.lib import FormattingTags, translate + + +class FormattingTagController(object): + """ + The :class:`FormattingTagController` manages the non UI functions . + """ + def __init__(self): + """ + Initiator + """ + self.html_tag_regex = re.compile(r'<(?:(?P/(?=[^\s/>]+>))?' + r'(?P[^\s/!\?>]+)(?:\s+[^\s=]+="[^"]*")*\s*(?P/)?' + r'|(?P!\[CDATA\[(?:(?!\]\]>).)*\]\])' + r'|(?P\?(?:(?!\?>).)*\?)' + r'|(?P!--(?:(?!-->).)*--))>', re.UNICODE) + self.html_regex = re.compile(r'^(?:[^<>]*%s)*[^<>]*$' % self.html_tag_regex.pattern) + + def pre_save(self): + """ + Cleanup the array before save validation runs + """ + self.protected_tags = [tag for tag in FormattingTags.html_expands if tag.get('protected')] + self.custom_tags = [] + + def validate_for_save(self, desc, tag, start_html, end_html): + """ + Validate a custom tag and add to the tags array if valid.. + + `desc` + Explanation of the tag. + + `tag` + The tag in the song used to mark the text. + + `start_html` + The start html tag. + + `end_html` + The end html tag. + + """ + for linenumber, html1 in enumerate(self.protected_tags): + if self._strip(html1['start tag']) == tag: + return translate('OpenLP.FormattingTagForm', 'Tag %s already defined.') % tag + if self._strip(html1['desc']) == desc: + return translate('OpenLP.FormattingTagForm', 'Description %s already defined.') % tag + for linenumber, html1 in enumerate(self.custom_tags): + if self._strip(html1['start tag']) == tag: + return translate('OpenLP.FormattingTagForm', 'Tag %s already defined.') % tag + if self._strip(html1['desc']) == desc: + return translate('OpenLP.FormattingTagForm', 'Description %s already defined.') % tag + tag = { + 'desc': desc, + 'start tag': '{%s}' % tag, + 'start html': start_html, + 'end tag': '{/%s}' % tag, + 'end html': end_html, + 'protected': False, + 'temporary': False + } + self.custom_tags.append(tag) + + def save_tags(self): + """ + Save the new tags if they are valid. + """ + FormattingTags.save_html_tags(self.custom_tags) + FormattingTags.load_tags() + + def _strip(self, tag): + """ + Remove tag wrappers for editing. + + `tag` + Tag to be stripped + """ + tag = tag.replace('{', '') + tag = tag.replace('}', '') + return tag + + def start_html_to_end_html(self, start_html): + """ + Return the end HTML for a given start HTML or None if invalid. + + `start_html` + The start html tag. + + """ + end_tags = [] + match = self.html_regex.match(start_html) + if match: + match = self.html_tag_regex.search(start_html) + while match: + if match.group('tag'): + tag = match.group('tag').lower() + if match.group('close'): + if match.group('empty') or not end_tags or end_tags.pop() != tag: + return + elif not match.group('empty'): + end_tags.append(tag) + match = self.html_tag_regex.search(start_html, match.end()) + return ''.join(map(lambda tag: '' % tag, reversed(end_tags))) + + def start_tag_changed(self, start_html, end_html): + """ + Validate the HTML tags when the start tag has been changed. + + `start_html` + The start html tag. + + `end_html` + The end html tag. + + """ + end = self.start_html_to_end_html(start_html) + if not end_html: + if not end: + return translate('OpenLP.FormattingTagForm', 'Start tag %s is not valid HTML' % start_html), None + return None, end + return None, None + + def end_tag_changed(self, start_html, end_html): + """ + Validate the HTML tags when the end tag has been changed. + + `start_html` + The start html tag. + + `end_html` + The end html tag. + + """ + end = self.start_html_to_end_html(start_html) + if not end_html: + return None, end + if end and end != end_html: + return translate('OpenLP.FormattingTagForm', + 'End tag %s does not match end tag for start tag %s' % (end, start_html)), None + return None, None \ No newline at end of file diff --git a/openlp/core/ui/formattingtagdialog.py b/openlp/core/ui/formattingtagdialog.py index 20c982f36..6d7dd2453 100644 --- a/openlp/core/ui/formattingtagdialog.py +++ b/openlp/core/ui/formattingtagdialog.py @@ -31,7 +31,7 @@ The UI widgets for the formatting tags window. """ from PyQt4 import QtCore, QtGui -from openlp.core.lib import UiStrings, translate +from openlp.core.lib import UiStrings, translate, build_icon from openlp.core.lib.ui import create_button_box @@ -45,12 +45,34 @@ class Ui_FormattingTagDialog(object): """ formatting_tag_dialog.setObjectName('formatting_tag_dialog') formatting_tag_dialog.resize(725, 548) - self.list_data_grid_layout = QtGui.QGridLayout(formatting_tag_dialog) + self.list_data_grid_layout = QtGui.QVBoxLayout(formatting_tag_dialog) self.list_data_grid_layout.setMargin(8) self.list_data_grid_layout.setObjectName('list_data_grid_layout') + self.tag_table_widget_read_label = QtGui.QLabel() + self.list_data_grid_layout.addWidget(self.tag_table_widget_read_label) + self.tag_table_widget_read = QtGui.QTableWidget(formatting_tag_dialog) + self.tag_table_widget_read.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.tag_table_widget_read.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) + self.tag_table_widget_read.setAlternatingRowColors(True) + self.tag_table_widget_read.setCornerButtonEnabled(False) + self.tag_table_widget_read.setObjectName('tag_table_widget_read') + self.tag_table_widget_read.setColumnCount(4) + self.tag_table_widget_read.setRowCount(0) + self.tag_table_widget_read.horizontalHeader().setStretchLastSection(True) + item = QtGui.QTableWidgetItem() + self.tag_table_widget_read.setHorizontalHeaderItem(0, item) + item = QtGui.QTableWidgetItem() + self.tag_table_widget_read.setHorizontalHeaderItem(1, item) + item = QtGui.QTableWidgetItem() + self.tag_table_widget_read.setHorizontalHeaderItem(2, item) + item = QtGui.QTableWidgetItem() + self.tag_table_widget_read.setHorizontalHeaderItem(3, item) + self.list_data_grid_layout.addWidget(self.tag_table_widget_read) + self.tag_table_widget_label = QtGui.QLabel() + self.list_data_grid_layout.addWidget(self.tag_table_widget_label) self.tag_table_widget = QtGui.QTableWidget(formatting_tag_dialog) self.tag_table_widget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.tag_table_widget.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) + self.tag_table_widget.setEditTriggers(QtGui.QAbstractItemView.AllEditTriggers) self.tag_table_widget.setAlternatingRowColors(True) self.tag_table_widget.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) self.tag_table_widget.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) @@ -67,59 +89,26 @@ class Ui_FormattingTagDialog(object): self.tag_table_widget.setHorizontalHeaderItem(2, item) item = QtGui.QTableWidgetItem() self.tag_table_widget.setHorizontalHeaderItem(3, item) - self.list_data_grid_layout.addWidget(self.tag_table_widget, 0, 0, 1, 1) - self.horizontal_layout = QtGui.QHBoxLayout() - self.horizontal_layout.setObjectName('horizontal_layout') - spacer_item = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.horizontal_layout.addItem(spacer_item) - self.delete_push_button = QtGui.QPushButton(formatting_tag_dialog) - self.delete_push_button.setObjectName('delete_push_button') - self.horizontal_layout.addWidget(self.delete_push_button) - self.list_data_grid_layout.addLayout(self.horizontal_layout, 1, 0, 1, 1) - self.edit_group_box = QtGui.QGroupBox(formatting_tag_dialog) - self.edit_group_box.setObjectName('edit_group_box') - self.data_grid_layout = QtGui.QGridLayout(self.edit_group_box) - self.data_grid_layout.setObjectName('data_grid_layout') - self.description_label = QtGui.QLabel(self.edit_group_box) - self.description_label.setAlignment(QtCore.Qt.AlignCenter) - self.description_label.setObjectName('description_label') - self.data_grid_layout.addWidget(self.description_label, 0, 0, 1, 1) - self.description_line_edit = QtGui.QLineEdit(self.edit_group_box) - self.description_line_edit.setObjectName('description_line_edit') - self.data_grid_layout.addWidget(self.description_line_edit, 0, 1, 2, 1) - self.new_push_button = QtGui.QPushButton(self.edit_group_box) - self.new_push_button.setObjectName('new_push_button') - self.data_grid_layout.addWidget(self.new_push_button, 0, 2, 2, 1) - self.tag_label = QtGui.QLabel(self.edit_group_box) - self.tag_label.setAlignment(QtCore.Qt.AlignCenter) - self.tag_label.setObjectName('tag_label') - self.data_grid_layout.addWidget(self.tag_label, 2, 0, 1, 1) - self.tag_line_edit = QtGui.QLineEdit(self.edit_group_box) - self.tag_line_edit.setMaximumSize(QtCore.QSize(50, 16777215)) - self.tag_line_edit.setMaxLength(5) - self.tag_line_edit.setObjectName('tag_line_edit') - self.data_grid_layout.addWidget(self.tag_line_edit, 2, 1, 1, 1) - self.start_tag_label = QtGui.QLabel(self.edit_group_box) - self.start_tag_label.setAlignment(QtCore.Qt.AlignCenter) - self.start_tag_label.setObjectName('start_tag_label') - self.data_grid_layout.addWidget(self.start_tag_label, 3, 0, 1, 1) - self.start_tag_line_edit = QtGui.QLineEdit(self.edit_group_box) - self.start_tag_line_edit.setObjectName('start_tag_line_edit') - self.data_grid_layout.addWidget(self.start_tag_line_edit, 3, 1, 1, 1) - self.end_tag_label = QtGui.QLabel(self.edit_group_box) - self.end_tag_label.setAlignment(QtCore.Qt.AlignCenter) - self.end_tag_label.setObjectName('end_tag_label') - self.data_grid_layout.addWidget(self.end_tag_label, 4, 0, 1, 1) - self.end_tag_line_edit = QtGui.QLineEdit(self.edit_group_box) - self.end_tag_line_edit.setObjectName('end_tag_line_edit') - self.data_grid_layout.addWidget(self.end_tag_line_edit, 4, 1, 1, 1) - self.save_push_button = QtGui.QPushButton(self.edit_group_box) - self.save_push_button.setObjectName('save_push_button') - self.data_grid_layout.addWidget(self.save_push_button, 4, 2, 1, 1) - self.list_data_grid_layout.addWidget(self.edit_group_box, 2, 0, 1, 1) - self.button_box = create_button_box(formatting_tag_dialog, 'button_box', ['close']) - self.list_data_grid_layout.addWidget(self.button_box, 3, 0, 1, 1) - + self.list_data_grid_layout.addWidget(self.tag_table_widget) + self.edit_button_layout = QtGui.QHBoxLayout() + self.new_button = QtGui.QPushButton(formatting_tag_dialog) + self.new_button.setIcon(build_icon(':/general/general_new.png')) + self.new_button.setObjectName('new_button') + self.edit_button_layout.addWidget(self.new_button) + self.delete_button = QtGui.QPushButton(formatting_tag_dialog) + self.delete_button.setIcon(build_icon(':/general/general_delete.png')) + self.delete_button.setObjectName('delete_button') + self.edit_button_layout.addWidget(self.delete_button) + self.edit_button_layout.addStretch() + self.list_data_grid_layout.addLayout(self.edit_button_layout) + self.button_box = create_button_box(formatting_tag_dialog, 'button_box', + ['cancel', 'save', 'defaults']) + self.save_button = self.button_box.button(QtGui.QDialogButtonBox.Save) + self.save_button.setObjectName('save_button') + self.restore_button = self.button_box.button(QtGui.QDialogButtonBox.RestoreDefaults) + self.restore_button.setIcon(build_icon(':/general/general_revert.png')) + self.restore_button.setObjectName('restore_button') + self.list_data_grid_layout.addWidget(self.button_box) self.retranslateUi(formatting_tag_dialog) def retranslateUi(self, formatting_tag_dialog): @@ -127,14 +116,19 @@ class Ui_FormattingTagDialog(object): Translate the UI on the fly """ formatting_tag_dialog.setWindowTitle(translate('OpenLP.FormattingTagDialog', 'Configure Formatting Tags')) - self.edit_group_box.setTitle(translate('OpenLP.FormattingTagDialog', 'Edit Selection')) - self.save_push_button.setText(translate('OpenLP.FormattingTagDialog', 'Save')) - self.description_label.setText(translate('OpenLP.FormattingTagDialog', 'Description')) - self.tag_label.setText(translate('OpenLP.FormattingTagDialog', 'Tag')) - self.start_tag_label.setText(translate('OpenLP.FormattingTagDialog', 'Start HTML')) - self.end_tag_label.setText(translate('OpenLP.FormattingTagDialog', 'End HTML')) - self.delete_push_button.setText(UiStrings().Delete) - self.new_push_button.setText(UiStrings().New) + self.delete_button.setText(UiStrings().Delete) + self.new_button.setText(UiStrings().New) + self.tag_table_widget_read_label.setText(translate('OpenLP.FormattingTagDialog', 'Default Formatting')) + self.tag_table_widget_read.horizontalHeaderItem(0).\ + setText(translate('OpenLP.FormattingTagDialog', 'Description')) + self.tag_table_widget_read.horizontalHeaderItem(1).setText(translate('OpenLP.FormattingTagDialog', 'Tag')) + self.tag_table_widget_read.horizontalHeaderItem(2).\ + setText(translate('OpenLP.FormattingTagDialog', 'Start HTML')) + self.tag_table_widget_read.horizontalHeaderItem(3).setText(translate('OpenLP.FormattingTagDialog', 'End HTML')) + self.tag_table_widget_read.setColumnWidth(0, 120) + self.tag_table_widget_read.setColumnWidth(1, 80) + self.tag_table_widget_read.setColumnWidth(2, 330) + self.tag_table_widget_label.setText(translate('OpenLP.FormattingTagDialog', 'Custom Formatting')) self.tag_table_widget.horizontalHeaderItem(0).setText(translate('OpenLP.FormattingTagDialog', 'Description')) self.tag_table_widget.horizontalHeaderItem(1).setText(translate('OpenLP.FormattingTagDialog', 'Tag')) self.tag_table_widget.horizontalHeaderItem(2).setText(translate('OpenLP.FormattingTagDialog', 'Start HTML')) diff --git a/openlp/core/ui/formattingtagform.py b/openlp/core/ui/formattingtagform.py index d61eab23d..c6906fc95 100644 --- a/openlp/core/ui/formattingtagform.py +++ b/openlp/core/ui/formattingtagform.py @@ -31,14 +31,25 @@ The :mod:`formattingtagform` provides an Tag Edit facility. The Base set are pro Custom tags can be defined and saved. The Custom Tag arrays are saved in a json string so QSettings works on them. Base Tags cannot be changed. """ + from PyQt4 import QtGui from openlp.core.lib import FormattingTags, translate -from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui.formattingtagdialog import Ui_FormattingTagDialog +from openlp.core.ui.formattingtagcontroller import FormattingTagController -class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): +class EditColumn(object): + """ + Hides the magic numbers for the table columns + """ + Description = 0 + Tag = 1 + StartHtml = 2 + EndHtml = 3 + + +class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog, FormattingTagController): """ The :class:`FormattingTagForm` manages the settings tab . """ @@ -48,17 +59,17 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): """ super(FormattingTagForm, self).__init__(parent) self.setupUi(self) + self.services = FormattingTagController() self.tag_table_widget.itemSelectionChanged.connect(self.on_row_selected) - self.new_push_button.clicked.connect(self.on_new_clicked) - self.save_push_button.clicked.connect(self.on_saved_clicked) - self.delete_push_button.clicked.connect(self.on_delete_clicked) + self.new_button.clicked.connect(self.on_new_clicked) + #self.save_button.clicked.connect(self.on_saved_clicked) + self.delete_button.clicked.connect(self.on_delete_clicked) + self.tag_table_widget.currentCellChanged.connect(self.on_current_cell_changed) self.button_box.rejected.connect(self.close) - self.description_line_edit.textEdited.connect(self.on_text_edited) - self.tag_line_edit.textEdited.connect(self.on_text_edited) - self.start_tag_line_edit.textEdited.connect(self.on_text_edited) - self.end_tag_line_edit.textEdited.connect(self.on_text_edited) # Forces reloading of tags from openlp configuration. FormattingTags.load_tags() + self.is_deleting = False + self.reloading = False def exec_(self): """ @@ -66,138 +77,128 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog): """ # Create initial copy from master self._reloadTable() - self.selected = -1 return QtGui.QDialog.exec_(self) def on_row_selected(self): """ Table Row selected so display items and set field state. """ - self.save_push_button.setEnabled(False) - self.selected = self.tag_table_widget.currentRow() - html = FormattingTags.get_html_tags()[self.selected] - self.description_line_edit.setText(html['desc']) - self.tag_line_edit.setText(self._strip(html['start tag'])) - self.start_tag_line_edit.setText(html['start html']) - self.end_tag_line_edit.setText(html['end html']) - if html['protected']: - self.description_line_edit.setEnabled(False) - self.tag_line_edit.setEnabled(False) - self.start_tag_line_edit.setEnabled(False) - self.end_tag_line_edit.setEnabled(False) - self.delete_push_button.setEnabled(False) - else: - self.description_line_edit.setEnabled(True) - self.tag_line_edit.setEnabled(True) - self.start_tag_line_edit.setEnabled(True) - self.end_tag_line_edit.setEnabled(True) - self.delete_push_button.setEnabled(True) - - def on_text_edited(self, text): - """ - Enable the ``save_push_button`` when any of the selected tag's properties - has been changed. - """ - self.save_push_button.setEnabled(True) + self.delete_button.setEnabled(True) def on_new_clicked(self): """ - Add a new tag to list only if it is not a duplicate. + Add a new tag to edit list and select it for editing. """ - for html in FormattingTags.get_html_tags(): - if self._strip(html['start tag']) == 'n': - critical_error_message_box( - translate('OpenLP.FormattingTagForm', 'Update Error'), - translate('OpenLP.FormattingTagForm', 'Tag "n" already defined.')) - return - # Add new tag to list - tag = { - 'desc': translate('OpenLP.FormattingTagForm', 'New Tag'), - 'start tag': '{n}', - 'start html': translate('OpenLP.FormattingTagForm', ''), - 'end tag': '{/n}', - 'end html': translate('OpenLP.FormattingTagForm', ''), - 'protected': False, - 'temporary': False - } - FormattingTags.add_html_tags([tag]) - FormattingTags.save_html_tags() - self._reloadTable() - # Highlight new row - self.tag_table_widget.selectRow(self.tag_table_widget.rowCount() - 1) - self.on_row_selected() + new_row = self.tag_table_widget.rowCount() + self.tag_table_widget.insertRow(new_row) + self.tag_table_widget.setItem(new_row, 0, + QtGui.QTableWidgetItem(translate('OpenLP.FormattingTagForm', 'New Tag%s') % str(new_row))) + self.tag_table_widget.setItem(new_row, 1, QtGui.QTableWidgetItem('n%s' % str(new_row))) + self.tag_table_widget.setItem(new_row, 2, + QtGui.QTableWidgetItem(translate('OpenLP.FormattingTagForm', ''))) + self.tag_table_widget.setItem(new_row, 3, QtGui.QTableWidgetItem('')) + self.tag_table_widget.resizeRowsToContents() self.tag_table_widget.scrollToBottom() + self.tag_table_widget.selectRow(new_row) def on_delete_clicked(self): """ - Delete selected custom tag. + Delete selected custom row. """ - if self.selected != -1: - FormattingTags.remove_html_tag(self.selected) - # As the first items are protected we should not have to take care - # of negative indexes causing tracebacks. - self.tag_table_widget.selectRow(self.selected - 1) - self.selected = -1 - FormattingTags.save_html_tags() - self._reloadTable() + selected = self.tag_table_widget.currentRow() + if selected != -1: + self.is_deleting = True + self.tag_table_widget.removeRow(selected) - def on_saved_clicked(self): + def accept(self): """ Update Custom Tag details if not duplicate and save the data. """ - html_expands = FormattingTags.get_html_tags() - if self.selected != -1: - html = html_expands[self.selected] - tag = self.tag_line_edit.text() - for linenumber, html1 in enumerate(html_expands): - if self._strip(html1['start tag']) == tag and linenumber != self.selected: - critical_error_message_box( - translate('OpenLP.FormattingTagForm', 'Update Error'), - translate('OpenLP.FormattingTagForm', 'Tag %s already defined.') % tag) - return - html['desc'] = self.description_line_edit.text() - html['start html'] = self.start_tag_line_edit.text() - html['end html'] = self.end_tag_line_edit.text() - html['start tag'] = '{%s}' % tag - html['end tag'] = '{/%s}' % tag - # Keep temporary tags when the user changes one. - html['temporary'] = False - self.selected = -1 - FormattingTags.save_html_tags() - self._reloadTable() + count = 0 + self.services.pre_save() + while count < self.tag_table_widget.rowCount(): + error = self.services.validate_for_save(self.tag_table_widget.item(count, 0).text(), + self.tag_table_widget.item(count, 1).text(), self.tag_table_widget.item(count, 2).text(), + self.tag_table_widget.item(count, 3).text()) + if error: + QtGui.QMessageBox.warning(self, + translate('OpenLP.FormattingTagForm', 'Validation Error'), error, QtGui.QMessageBox.Ok) + self.tag_table_widget.selectRow(count) + return + count += 1 + self.services.save_tags() + QtGui.QDialog.accept(self) def _reloadTable(self): """ Reset List for loading. """ + self.reloading = True + self.tag_table_widget_read.clearContents() + self.tag_table_widget_read.setRowCount(0) self.tag_table_widget.clearContents() self.tag_table_widget.setRowCount(0) - self.new_push_button.setEnabled(True) - self.save_push_button.setEnabled(False) - self.delete_push_button.setEnabled(False) + self.new_button.setEnabled(True) + self.delete_button.setEnabled(False) for linenumber, html in enumerate(FormattingTags.get_html_tags()): - self.tag_table_widget.setRowCount(self.tag_table_widget.rowCount() + 1) - self.tag_table_widget.setItem(linenumber, 0, QtGui.QTableWidgetItem(html['desc'])) - self.tag_table_widget.setItem(linenumber, 1, QtGui.QTableWidgetItem(self._strip(html['start tag']))) - self.tag_table_widget.setItem(linenumber, 2, QtGui.QTableWidgetItem(html['start html'])) - self.tag_table_widget.setItem(linenumber, 3, QtGui.QTableWidgetItem(html['end html'])) - # Permanent (persistent) tags do not have this key. - if 'temporary' not in html: + if html['protected']: + line = self.tag_table_widget_read.rowCount() + self.tag_table_widget_read.setRowCount(line + 1) + self.tag_table_widget_read.setItem(line, 0, QtGui.QTableWidgetItem(html['desc'])) + self.tag_table_widget_read.setItem(line, 1, QtGui.QTableWidgetItem(self._strip(html['start tag']))) + self.tag_table_widget_read.setItem(line, 2, QtGui.QTableWidgetItem(html['start html'])) + self.tag_table_widget_read.setItem(line, 3, QtGui.QTableWidgetItem(html['end html'])) + self.tag_table_widget_read.resizeRowsToContents() + else: + line = self.tag_table_widget.rowCount() + self.tag_table_widget.setRowCount(line + 1) + self.tag_table_widget.setItem(line, 0, QtGui.QTableWidgetItem(html['desc'])) + self.tag_table_widget.setItem(line, 1, QtGui.QTableWidgetItem(self._strip(html['start tag']))) + self.tag_table_widget.setItem(line, 2, QtGui.QTableWidgetItem(html['start html'])) + self.tag_table_widget.setItem(line, 3, QtGui.QTableWidgetItem(html['end html'])) + self.tag_table_widget.resizeRowsToContents() + # Permanent (persistent) tags do not have this key html['temporary'] = False - self.tag_table_widget.resizeRowsToContents() - self.description_line_edit.setText('') - self.tag_line_edit.setText('') - self.start_tag_line_edit.setText('') - self.end_tag_line_edit.setText('') - self.description_line_edit.setEnabled(False) - self.tag_line_edit.setEnabled(False) - self.start_tag_line_edit.setEnabled(False) - self.end_tag_line_edit.setEnabled(False) + self.reloading = False - def _strip(self, tag): + def on_current_cell_changed(self, cur_row, cur_col, pre_row, pre_col): """ - Remove tag wrappers for editing. + This function processes all user edits in the table. It is called on each cell change. """ - tag = tag.replace('{', '') - tag = tag.replace('}', '') - return tag + if self.is_deleting: + self.is_deleting = False + return + if self.reloading: + return + # only process for editable rows + if self.tag_table_widget.item(pre_row, 0): + item = self.tag_table_widget.item(pre_row, pre_col) + text = item.text() + errors = None + if pre_col is EditColumn.Description: + if not text: + errors = translate('OpenLP.FormattingTagForm', 'Description is missing') + elif pre_col is EditColumn.Tag: + if not text: + errors = translate('OpenLP.FormattingTagForm', 'Tag is missing') + elif pre_col is EditColumn.StartHtml: + # HTML edited + item = self.tag_table_widget.item(pre_row, 3) + end_html = item.text() + errors, tag = self.services.start_tag_changed(text, end_html) + if tag: + self.tag_table_widget.setItem(pre_row, 3, QtGui.QTableWidgetItem(tag)) + self.tag_table_widget.resizeRowsToContents() + elif pre_col is EditColumn.EndHtml: + # HTML edited + item = self.tag_table_widget.item(pre_row, 2) + start_html = item.text() + errors, tag = self.services.end_tag_changed(start_html, text) + if tag: + self.tag_table_widget.setItem(pre_row, 3, QtGui.QTableWidgetItem(tag)) + if errors: + QtGui.QMessageBox.warning(self, + translate('OpenLP.FormattingTagForm', 'Validation Error'), errors, QtGui.QMessageBox.Ok) + #self.tag_table_widget.selectRow(pre_row - 1) + self.tag_table_widget.resizeRowsToContents() + diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index 747988583..66f71545f 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -692,7 +692,6 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog): self.verse_edit_button.setEnabled(False) self.verse_delete_button.setEnabled(False) - def on_verse_order_text_changed(self, text): """ Checks if the verse order is complete or missing. Shows a error message according to the state of the verse diff --git a/openlp/plugins/songs/lib/songshowplusimport.py b/openlp/plugins/songs/lib/songshowplusimport.py index dad5ecfc0..35cd44b8a 100644 --- a/openlp/plugins/songs/lib/songshowplusimport.py +++ b/openlp/plugins/songs/lib/songshowplusimport.py @@ -30,13 +30,14 @@ The :mod:`songshowplusimport` module provides the functionality for importing SongShow Plus songs into the OpenLP database. """ +import chardet import os import logging import re import struct from openlp.core.ui.wizard import WizardStrings -from openlp.plugins.songs.lib import VerseType +from openlp.plugins.songs.lib import VerseType, retrieve_windows_encoding from openlp.plugins.songs.lib.songimport import SongImport TITLE = 1 @@ -132,41 +133,43 @@ class SongShowPlusImport(SongImport): else: length_descriptor, = struct.unpack("B", song_data.read(1)) log.debug(length_descriptor_size) - data = song_data.read(length_descriptor).decode() + data = song_data.read(length_descriptor) if block_key == TITLE: - self.title = data + self.title = self.decode(data) elif block_key == AUTHOR: - authors = data.split(" / ") + authors = self.decode(data).split(" / ") for author in authors: if author.find(",") !=-1: authorParts = author.split(", ") author = authorParts[1] + " " + authorParts[0] self.parse_author(author) elif block_key == COPYRIGHT: - self.addCopyright(data) + self.addCopyright(self.decode(data)) elif block_key == CCLI_NO: self.ccliNumber = int(data) elif block_key == VERSE: - self.addVerse(data, "%s%s" % (VerseType.tags[VerseType.Verse], verse_no)) + self.addVerse(self.decode(data), "%s%s" % (VerseType.tags[VerseType.Verse], verse_no)) elif block_key == CHORUS: - self.addVerse(data, "%s%s" % (VerseType.tags[VerseType.Chorus], verse_no)) + self.addVerse(self.decode(data), "%s%s" % (VerseType.tags[VerseType.Chorus], verse_no)) elif block_key == BRIDGE: - self.addVerse(data, "%s%s" % (VerseType.tags[VerseType.Bridge], verse_no)) + self.addVerse(self.decode(data), "%s%s" % (VerseType.tags[VerseType.Bridge], verse_no)) elif block_key == TOPIC: - self.topics.append(data) + self.topics.append(self.decode(data)) elif block_key == COMMENTS: - self.comments = data + self.comments = self.decode(data) elif block_key == VERSE_ORDER: - verse_tag = self.to_openlp_verse_tag(data, True) + verse_tag = self.to_openlp_verse_tag(self.decode(data), True) if verse_tag: + if not isinstance(verse_tag, str): + verse_tag = self.decode(verse_tag) self.ssp_verse_order_list.append(verse_tag) elif block_key == SONG_BOOK: - self.songBookName = data + self.songBookName = self.decode(data) elif block_key == SONG_NUMBER: self.songNumber = ord(data) elif block_key == CUSTOM_VERSE: verse_tag = self.to_openlp_verse_tag(verse_name) - self.addVerse(data, verse_tag) + self.addVerse(self.decode(data), verse_tag) else: log.debug("Unrecognised blockKey: %s, data: %s" % (block_key, data)) song_data.seek(next_block_starts) @@ -204,3 +207,9 @@ class SongShowPlusImport(SongImport): verse_tag = VerseType.tags[VerseType.Other] verse_number = self.other_list[verse_name] return verse_tag + verse_number + + def decode(self, data): + try: + return str(data, chardet.detect(data)['encoding']) + except: + return str(data, retrieve_windows_encoding()) \ No newline at end of file diff --git a/tests/functional/openlp_core_ui/tests_formattingtagscontroller.py b/tests/functional/openlp_core_ui/tests_formattingtagscontroller.py new file mode 100644 index 000000000..dc9495775 --- /dev/null +++ b/tests/functional/openlp_core_ui/tests_formattingtagscontroller.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2013 Raoul Snyman # +# Portions copyright (c) 2008-2013 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 # +############################################################################### +""" +Package to test the openlp.core.ui.formattingtagscontroller package. +""" +from unittest import TestCase + +from openlp.core.ui import FormattingTagController + + +class TestFormattingTagController(TestCase): + + def setUp(self): + self.services = FormattingTagController() + + def test_strip(self): + """ + Test that the _strip strips the correct chars + """ + # GIVEN: An instance of the Formatting Tag Form and a string containing a tag + tag = '{tag}' + + # WHEN: Calling _strip + result = self.services._strip(tag) + + # THEN: The tag should be returned with the wrappers removed. + self.assertEqual(result, 'tag', 'FormattingTagForm._strip should return u\'tag\' when called with u\'{tag}\'') + + def test_end_tag_changed_processes_correctly(self): + """ + Test that the end html tags are generated correctly + """ + # GIVEN: A list of start , end tags and error messages + tests = [] + test = {'start': '', 'end': None, 'gen': '', 'valid': None} + tests.append(test) + test = {'start': '', 'end': '', 'gen': None, 'valid': None} + tests.append(test) + test = {'start': '', 'end': '', 'gen': None, + 'valid': 'End tag does not match end tag for start tag '} + tests.append(test) + + # WHEN: Testing each one of them in turn + for test in tests: + error, result = self.services.end_tag_changed(test['start'], test['end']) + + # THEN: The result should match the predetermined value. + self.assertTrue(result == test['gen'], + 'Function should handle end tag correctly : %s and %s for %s ' % (test['gen'], result, test['start'])) + self.assertTrue(error == test['valid'], + 'Function should not generate unexpected error messages : %s ' % error) + + def test_start_tag_changed_processes_correctly(self): + """ + Test that the end html tags are generated correctly + """ + # GIVEN: A list of start , end tags and error messages + tests = [] + test = {'start': '', 'end': '', 'gen': '', 'valid': None} + tests.append(test) + test = {'start': '', 'end': '', 'gen': None, 'valid': None} + tests.append(test) + test = {'start': 'superfly', 'end': '', 'gen': None, 'valid': 'Start tag superfly is not valid HTML'} + tests.append(test) + + # WHEN: Testing each one of them in turn + for test in tests: + error, result = self.services.start_tag_changed(test['start'], test['end']) + + # THEN: The result should match the predetermined value. + self.assertTrue(result == test['gen'], + 'Function should handle end tag correctly : %s and %s ' % (test['gen'], result)) + self.assertTrue(error == test['valid'], + 'Function should not generate unexpected error messages : %s ' % error) + + def test_start_html_to_end_html(self): + """ + Test that the end html tags are generated correctly + """ + # GIVEN: A list of valid and invalid tags + tests = {'': '', '': '', 'superfly': '', '': None, + '': ''} + + # WHEN: Testing each one of them + for test1, test2 in tests.items(): + result = self.services.start_html_to_end_html(test1) + + # THEN: The result should match the predetermined value. + self.assertTrue(result == test2, 'Calculated end tag should be valid: %s and %s = %s' + % (test1, test2, result)) \ No newline at end of file diff --git a/tests/functional/openlp_core_ui/tests_formattingtagsform.py b/tests/functional/openlp_core_ui/tests_formattingtagsform.py new file mode 100644 index 000000000..7dc3d4b6f --- /dev/null +++ b/tests/functional/openlp_core_ui/tests_formattingtagsform.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2013 Raoul Snyman # +# Portions copyright (c) 2008-2013 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 # +############################################################################### +""" +Package to test the openlp.core.ui.formattingtagsform package. +""" +from unittest import TestCase + +from tests.functional import MagicMock, patch + +from openlp.core.ui.formattingtagform import FormattingTagForm + +# TODO: Tests Still TODO +# __init__ +# exec_ +# on_new_clicked +# on_delete_clicked +# on_saved_clicked +# _reloadTable + + +class TestFormattingTagForm(TestCase): + + def setUp(self): + self.init_patcher = patch('openlp.core.ui.formattingtagform.FormattingTagForm.__init__') + self.qdialog_patcher = patch('openlp.core.ui.formattingtagform.QtGui.QDialog') + self.ui_formatting_tag_dialog_patcher = patch('openlp.core.ui.formattingtagform.Ui_FormattingTagDialog') + self.mocked_init = self.init_patcher.start() + self.mocked_qdialog = self.qdialog_patcher.start() + self.mocked_ui_formatting_tag_dialog = self.ui_formatting_tag_dialog_patcher.start() + self.mocked_init.return_value = None + + def tearDown(self): + self.init_patcher.stop() + self.qdialog_patcher.stop() + self.ui_formatting_tag_dialog_patcher.stop() + + def test_on_text_edited(self): + """ + Test that the appropriate actions are preformed when on_text_edited is called + """ + + # GIVEN: An instance of the Formatting Tag Form and a mocked save_push_button + form = FormattingTagForm() + form.save_button = MagicMock() + + # WHEN: on_text_edited is called with an arbitrary value + #form.on_text_edited('text') + + # THEN: setEnabled and setDefault should have been called on save_push_button + #form.save_button.setEnabled.assert_called_with(True) + diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py index 0e707414e..83cc24011 100644 --- a/tests/functional/openlp_core_utils/test_utils.py +++ b/tests/functional/openlp_core_utils/test_utils.py @@ -150,35 +150,14 @@ class TestUtils(TestCase): # THEN: The file name should be cleaned. self.assertEqual(wanted_name, result, 'The file name should not contain any special characters.') - def get_locale_key_windows_test(self): + def get_locale_key_test(self): """ - Test the get_locale_key(string) function on Windows + Test the get_locale_key(string) function """ - with patch('openlp.core.utils.languagemanager.LanguageManager.get_language') as mocked_get_language, \ - patch('openlp.core.utils.os') as mocked_os: + with patch('openlp.core.utils.languagemanager.LanguageManager.get_language') as mocked_get_language: # GIVEN: The language is German # 0x00C3 (A with diaresis) should be sorted as "A". 0x00DF (sharp s) should be sorted as "ss". mocked_get_language.return_value = 'de' - mocked_os.name = 'nt' - unsorted_list = ['Auszug', 'Aushang', '\u00C4u\u00DFerung'] - - # WHEN: We sort the list and use get_locale_key() to generate the sorting keys - sorted_list = sorted(unsorted_list, key=get_locale_key) - - # THEN: We get a properly sorted list - self.assertEqual(['Aushang', '\u00C4u\u00DFerung', 'Auszug'], sorted_list, - 'Strings should be sorted properly') - - def get_locale_key_linux_test(self): - """ - Test the get_locale_key(string) function on Linux - """ - with patch('openlp.core.utils.languagemanager.LanguageManager.get_language') as mocked_get_language, \ - patch('openlp.core.utils.os.name') as mocked_os: - # GIVEN: The language is German - # 0x00C3 (A with diaresis) should be sorted as "A". 0x00DF (sharp s) should be sorted as "ss". - mocked_get_language.return_value = 'de' - mocked_os.name = 'linux' unsorted_list = ['Auszug', 'Aushang', '\u00C4u\u00DFerung'] # WHEN: We sort the list and use get_locale_key() to generate the sorting keys