← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~m2j/openlp/formattingtags into lp:openlp

 

Meinert Jordan has proposed merging lp:~m2j/openlp/formattingtags into lp:openlp.

Commit message:
formatting tag editor reworked

Requested reviews:
  OpenLP Core (openlp-core)

For more details, see:
https://code.launchpad.net/~m2j/openlp/formattingtags/+merge/140132

I forgot about this branch, and didn't realized, that it was never merged. Now I merged it to the current trunk and did a rework.

This branch modifies the formatting tag editor in several ways:
- the user gets restricted to valid HTML.
- the closing tag is derived from the opening tag
- editing happens inline in the table

The result looks rather minimalistic, as I would like to shift the editor to the settings dialog. But before I want to discuss this. I'd like to make such a change in a seperated merge request.
-- 
https://code.launchpad.net/~m2j/openlp/formattingtags/+merge/140132
Your team OpenLP Core is requested to review the proposed merge of lp:~m2j/openlp/formattingtags into lp:openlp.
=== modified file 'openlp/core/ui/formattingtagdialog.py'
--- openlp/core/ui/formattingtagdialog.py	2012-12-01 07:57:54 +0000
+++ openlp/core/ui/formattingtagdialog.py	2012-12-17 11:31:23 +0000
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
 
 ###############################################################################
 # OpenLP - Open Source Lyrics Projection                                      #
@@ -30,120 +30,66 @@
 from PyQt4 import QtCore, QtGui
 
 from openlp.core.lib import translate
-from openlp.core.lib.ui import UiStrings, create_button_box
+from openlp.core.lib.ui import create_button_box
 
 class Ui_FormattingTagDialog(object):
 
-    def setupUi(self, formattingTagDialog):
-        formattingTagDialog.setObjectName(u'formattingTagDialog')
-        formattingTagDialog.resize(725, 548)
-        self.listdataGridLayout = QtGui.QGridLayout(formattingTagDialog)
-        self.listdataGridLayout.setMargin(8)
-        self.listdataGridLayout.setObjectName(u'listdataGridLayout')
-        self.tagTableWidget = QtGui.QTableWidget(formattingTagDialog)
-        self.tagTableWidget.setHorizontalScrollBarPolicy(
-            QtCore.Qt.ScrollBarAlwaysOff)
-        self.tagTableWidget.setEditTriggers(
-            QtGui.QAbstractItemView.NoEditTriggers)
-        self.tagTableWidget.setAlternatingRowColors(True)
-        self.tagTableWidget.setSelectionMode(
-            QtGui.QAbstractItemView.SingleSelection)
-        self.tagTableWidget.setSelectionBehavior(
-            QtGui.QAbstractItemView.SelectRows)
-        self.tagTableWidget.setCornerButtonEnabled(False)
-        self.tagTableWidget.setObjectName(u'tagTableWidget')
-        self.tagTableWidget.setColumnCount(4)
-        self.tagTableWidget.setRowCount(0)
-        self.tagTableWidget.horizontalHeader().setStretchLastSection(True)
-        item = QtGui.QTableWidgetItem()
-        self.tagTableWidget.setHorizontalHeaderItem(0, item)
-        item = QtGui.QTableWidgetItem()
-        self.tagTableWidget.setHorizontalHeaderItem(1, item)
-        item = QtGui.QTableWidgetItem()
-        self.tagTableWidget.setHorizontalHeaderItem(2, item)
-        item = QtGui.QTableWidgetItem()
-        self.tagTableWidget.setHorizontalHeaderItem(3, item)
-        self.listdataGridLayout.addWidget(self.tagTableWidget, 0, 0, 1, 1)
-        self.horizontalLayout = QtGui.QHBoxLayout()
-        self.horizontalLayout.setObjectName(u'horizontalLayout')
-        spacerItem = QtGui.QSpacerItem(40, 20,
-            QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
-        self.horizontalLayout.addItem(spacerItem)
-        self.deletePushButton = QtGui.QPushButton(formattingTagDialog)
-        self.deletePushButton.setObjectName(u'deletePushButton')
-        self.horizontalLayout.addWidget(self.deletePushButton)
-        self.listdataGridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 1)
-        self.editGroupBox = QtGui.QGroupBox(formattingTagDialog)
-        self.editGroupBox.setObjectName(u'editGroupBox')
-        self.dataGridLayout = QtGui.QGridLayout(self.editGroupBox)
-        self.dataGridLayout.setObjectName(u'dataGridLayout')
-        self.descriptionLabel = QtGui.QLabel(self.editGroupBox)
-        self.descriptionLabel.setAlignment(QtCore.Qt.AlignCenter)
-        self.descriptionLabel.setObjectName(u'descriptionLabel')
-        self.dataGridLayout.addWidget(self.descriptionLabel, 0, 0, 1, 1)
-        self.descriptionLineEdit = QtGui.QLineEdit(self.editGroupBox)
-        self.descriptionLineEdit.setObjectName(u'descriptionLineEdit')
-        self.dataGridLayout.addWidget(self.descriptionLineEdit, 0, 1, 2, 1)
-        self.newPushButton = QtGui.QPushButton(self.editGroupBox)
-        self.newPushButton.setObjectName(u'newPushButton')
-        self.dataGridLayout.addWidget(self.newPushButton, 0, 2, 2, 1)
-        self.tagLabel = QtGui.QLabel(self.editGroupBox)
-        self.tagLabel.setAlignment(QtCore.Qt.AlignCenter)
-        self.tagLabel.setObjectName(u'tagLabel')
-        self.dataGridLayout.addWidget(self.tagLabel, 2, 0, 1, 1)
-        self.tagLineEdit = QtGui.QLineEdit(self.editGroupBox)
-        self.tagLineEdit.setMaximumSize(QtCore.QSize(50, 16777215))
-        self.tagLineEdit.setMaxLength(5)
-        self.tagLineEdit.setObjectName(u'tagLineEdit')
-        self.dataGridLayout.addWidget(self.tagLineEdit, 2, 1, 1, 1)
-        self.startTagLabel = QtGui.QLabel(self.editGroupBox)
-        self.startTagLabel.setAlignment(QtCore.Qt.AlignCenter)
-        self.startTagLabel.setObjectName(u'startTagLabel')
-        self.dataGridLayout.addWidget(self.startTagLabel, 3, 0, 1, 1)
-        self.startTagLineEdit = QtGui.QLineEdit(self.editGroupBox)
-        self.startTagLineEdit.setObjectName(u'startTagLineEdit')
-        self.dataGridLayout.addWidget(self.startTagLineEdit, 3, 1, 1, 1)
-        self.endTagLabel = QtGui.QLabel(self.editGroupBox)
-        self.endTagLabel.setAlignment(QtCore.Qt.AlignCenter)
-        self.endTagLabel.setObjectName(u'endTagLabel')
-        self.dataGridLayout.addWidget(self.endTagLabel, 4, 0, 1, 1)
-        self.endTagLineEdit = QtGui.QLineEdit(self.editGroupBox)
-        self.endTagLineEdit.setObjectName(u'endTagLineEdit')
-        self.dataGridLayout.addWidget(self.endTagLineEdit, 4, 1, 1, 1)
-        self.savePushButton = QtGui.QPushButton(self.editGroupBox)
-        self.savePushButton.setObjectName(u'savePushButton')
-        self.dataGridLayout.addWidget(self.savePushButton, 4, 2, 1, 1)
-        self.listdataGridLayout.addWidget(self.editGroupBox, 2, 0, 1, 1)
-        self.buttonBox = create_button_box(formattingTagDialog, 'buttonBox',
-            [u'close'])
-        self.listdataGridLayout.addWidget(self.buttonBox, 3, 0, 1, 1)
-
-        self.retranslateUi(formattingTagDialog)
-
-    def retranslateUi(self, formattingTagDialog):
-        formattingTagDialog.setWindowTitle(translate(
+    def setup_ui(self, formatting_tag_dialog):
+        formatting_tag_dialog.setObjectName(u'formatting_tag_dialog')
+        formatting_tag_dialog.resize(725, 548)
+        self.dialog_layout = QtGui.QVBoxLayout(formatting_tag_dialog)
+        self.dialog_layout.setMargin(8)
+        self.dialog_layout.setObjectName(u'dialog_layout')
+        self.tag_table_widget = FormattingTagTableWidget(formatting_tag_dialog)
+        self.tag_table_widget.setObjectName(u'tagTableWidget')
+        self.dialog_layout.addWidget(self.tag_table_widget)
+        self.button_box = create_button_box(formatting_tag_dialog, 'button_box',
+            [u'cancel', u'save', u'defaults'])
+        self.save_button = self.button_box.button(QtGui.QDialogButtonBox.Save)
+        self.save_button.setObjectName(u'save_button')
+        self.restore_button = self.button_box.button(QtGui.QDialogButtonBox.RestoreDefaults)
+        self.restore_button.setObjectName(u'restore_button')
+        self.dialog_layout.addWidget(self.button_box)
+
+        self.retranslate_ui(formatting_tag_dialog)
+
+    def retranslate_ui(self, formatting_tag_dialog):
+        formatting_tag_dialog.setWindowTitle(translate(
             'OpenLP.FormattingTagDialog', 'Configure Formatting Tags'))
-        self.editGroupBox.setTitle(
-            translate('OpenLP.FormattingTagDialog', 'Edit Selection'))
-        self.savePushButton.setText(
-            translate('OpenLP.FormattingTagDialog', 'Save'))
-        self.descriptionLabel.setText(
-            translate('OpenLP.FormattingTagDialog', 'Description'))
-        self.tagLabel.setText(translate('OpenLP.FormattingTagDialog', 'Tag'))
-        self.startTagLabel.setText(
-            translate('OpenLP.FormattingTagDialog', 'Start HTML'))
-        self.endTagLabel.setText(
-            translate('OpenLP.FormattingTagDialog', 'End HTML'))
-        self.deletePushButton.setText(UiStrings().Delete)
-        self.newPushButton.setText(UiStrings().New)
-        self.tagTableWidget.horizontalHeaderItem(0).setText(
-            translate('OpenLP.FormattingTagDialog', 'Description'))
-        self.tagTableWidget.horizontalHeaderItem(1).setText(
+        self.tag_table_widget.horizontalHeaderItem(0).setText(
             translate('OpenLP.FormattingTagDialog', 'Tag'))
-        self.tagTableWidget.horizontalHeaderItem(2).setText(
+        self.tag_table_widget.horizontalHeaderItem(1).setText(
+            translate('OpenLP.FormattingTagDialog', 'Description'))
+        self.tag_table_widget.horizontalHeaderItem(2).setText(
             translate('OpenLP.FormattingTagDialog', 'Start HTML'))
-        self.tagTableWidget.horizontalHeaderItem(3).setText(
+        self.tag_table_widget.horizontalHeaderItem(3).setText(
             translate('OpenLP.FormattingTagDialog', 'End HTML'))
-        self.tagTableWidget.setColumnWidth(0, 120)
-        self.tagTableWidget.setColumnWidth(1, 80)
-        self.tagTableWidget.setColumnWidth(2, 330)
+
+class FormattingTagTableWidget(QtGui.QTableWidget):
+    """
+    Class to for the formatting tag table widget.
+    """
+    def __init__(self, parent):
+        """
+    	Constructor
+    	"""
+        QtGui.QTableWidget.__init__(self, parent)
+        self.setEditTriggers(QtGui.QAbstractItemView.AllEditTriggers)
+        self.setAlternatingRowColors(True)
+        self.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
+        self.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
+        self.verticalHeader().setVisible(False)
+        self.setColumnCount(4)
+        for index in range(0, 4):
+            self.setHorizontalHeaderItem(index, QtGui.QTableWidgetItem())
+
+    def resizeEvent(self, event):
+        """
+    	Resize the start html column to cover remaining space.
+   	    """
+        QtGui.QTableWidget.resizeEvent(self, event)
+        if self.columnCount() == 4:
+            self.resizeColumnsToContents()
+            self.setColumnWidth(2, event.size().width() - self.columnWidth(0)
+                - self.columnWidth(1) - self.columnWidth(3))
+            self.resizeRowsToContents()

=== modified file 'openlp/core/ui/formattingtagform.py'
--- openlp/core/ui/formattingtagform.py	2012-12-01 07:57:54 +0000
+++ openlp/core/ui/formattingtagform.py	2012-12-17 11:31:23 +0000
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
 
 ###############################################################################
 # OpenLP - Open Source Lyrics Projection                                      #
@@ -32,10 +32,11 @@
 The Custom Tag arrays are saved in a pickle so QSettings works on them. Base
 Tags cannot be changed.
 """
+import re
+import cgi
 from PyQt4 import QtCore, QtGui
 
 from openlp.core.lib import translate, FormattingTags
-from openlp.core.lib.ui import critical_error_message_box
 from openlp.core.ui.formattingtagdialog import Ui_FormattingTagDialog
 
 
@@ -48,174 +49,225 @@
         Constructor
         """
         QtGui.QDialog.__init__(self, parent)
-        self.setupUi(self)
-        QtCore.QObject.connect(self.tagTableWidget,
-            QtCore.SIGNAL(u'itemSelectionChanged()'),self.onRowSelected)
-        QtCore.QObject.connect(self.newPushButton,
-            QtCore.SIGNAL(u'clicked()'), self.onNewClicked)
-        QtCore.QObject.connect(self.savePushButton,
-            QtCore.SIGNAL(u'clicked()'), self.onSavedClicked)
-        QtCore.QObject.connect(self.deletePushButton,
-            QtCore.SIGNAL(u'clicked()'), self.onDeleteClicked)
-        QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(u'rejected()'),
-            self.close)
-        QtCore.QObject.connect(self.descriptionLineEdit,
-            QtCore.SIGNAL(u'textEdited(QString)'), self.onTextEdited)
-        QtCore.QObject.connect(self.tagLineEdit,
-            QtCore.SIGNAL(u'textEdited(QString)'), self.onTextEdited)
-        QtCore.QObject.connect(self.startTagLineEdit,
-            QtCore.SIGNAL(u'textEdited(QString)'), self.onTextEdited)
-        QtCore.QObject.connect(self.endTagLineEdit,
-            QtCore.SIGNAL(u'textEdited(QString)'), self.onTextEdited)
-        # Forces reloading of tags from openlp configuration.
+        self.setup_ui(self)
+        QtCore.QObject.connect(self.tag_table_widget, QtCore.SIGNAL(u'currentCellChanged(int, int, int, int)'),
+            self.on_current_cell_changed)
+        QtCore.QObject.connect(self.tag_table_widget, QtCore.SIGNAL(u'itemChanged(QTableWidgetItem*)'),
+            self.on_data_change)
+        QtCore.QObject.connect(self.restore_button, QtCore.SIGNAL(u'clicked()'),
+            self.load_tag_table)
+        QtCore.QObject.connect(self, QtCore.SIGNAL(u'accepted()'),
+            self.save_tags)
+
+    def exec_(self):
+        """
+        Run the dialog.
+        """
         FormattingTags.load_tags()
-
-    def exec_(self):
-        """
-        Load Display and set field state.
-        """
-        # Create initial copy from master
-        self._reloadTable()
-        self.selected = -1
+        self.load_tag_table()
         return QtGui.QDialog.exec_(self)
 
-    def onRowSelected(self):
-        """
-        Table Row selected so display items and set field state.
-        """
-        self.savePushButton.setEnabled(False)
-        self.selected = self.tagTableWidget.currentRow()
-        html = FormattingTags.get_html_tags()[self.selected]
-        self.descriptionLineEdit.setText(html[u'desc'])
-        self.tagLineEdit.setText(self._strip(html[u'start tag']))
-        self.startTagLineEdit.setText(html[u'start html'])
-        self.endTagLineEdit.setText(html[u'end html'])
-        if html[u'protected']:
-            self.descriptionLineEdit.setEnabled(False)
-            self.tagLineEdit.setEnabled(False)
-            self.startTagLineEdit.setEnabled(False)
-            self.endTagLineEdit.setEnabled(False)
-            self.deletePushButton.setEnabled(False)
-        else:
-            self.descriptionLineEdit.setEnabled(True)
-            self.tagLineEdit.setEnabled(True)
-            self.startTagLineEdit.setEnabled(True)
-            self.endTagLineEdit.setEnabled(True)
-            self.deletePushButton.setEnabled(True)
-
-    def onTextEdited(self, text):
-        """
-        Enable the ``savePushButton`` when any of the selected tag's properties
-        has been changed.
-        """
-        self.savePushButton.setEnabled(True)
-
-    def onNewClicked(self):
-        """
-        Add a new tag to list only if it is not a duplicate.
-        """
-        for html in FormattingTags.get_html_tags():
-            if self._strip(html[u'start tag']) == u'n':
-                critical_error_message_box(
-                    translate('OpenLP.FormattingTagForm', 'Update Error'),
+    def load_tag_table(self):
+        """
+        Load tags from the Settings into the table widget.
+        """
+        # clear table
+        self.tag_table_widget.setRowCount(0)
+        # add tags
+        tags = FormattingTags.get_html_tags()
+        for row, tag in enumerate(tags):
+            self.tag_table_widget.insertRow(row)
+            item = QtGui.QTableWidgetItem(tag[u'start tag'].strip(u'{}'))
+            flags = item.flags()
+            if not tag.get(u'protected'):
+                item.setData(QtCore.Qt.UserRole, {
+                    u'tag':tag[u'start tag'].strip(u'{}'),
+                    u'html':tag[u'start html'],
+                    u'description':tag[u'desc']})
+            else:
+                flags &= ~QtCore.Qt.ItemIsEditable
+                item.setFlags(flags)
+            self.tag_table_widget.setItem(row, 0, item)
+            item = QtGui.QTableWidgetItem(tag[u'desc'])
+            item.setToolTip(cgi.escape(tag[u'desc']))
+            item.setFlags(flags)
+            self.tag_table_widget.setItem(row, 1, item)
+            item = QtGui.QTableWidgetItem(tag[u'start html'])
+            item.setToolTip(cgi.escape(tag[u'start html']))
+            item.setFlags(flags)
+            self.tag_table_widget.setItem(row, 2, item)
+            item = QtGui.QTableWidgetItem(tag[u'end html'])
+            item.setToolTip(cgi.escape(tag[u'end html']))
+            item.setFlags(flags & ~QtCore.Qt.ItemIsEditable)
+            self.tag_table_widget.setItem(row, 3, item)
+        # add a empty row to allow definition of new tags
+        row = len(tags)
+        self.tag_table_widget.insertRow(row)
+        item = QtGui.QTableWidgetItem(u'')
+        self.tag_table_widget.setItem(row, 0, item)
+        flags = item.flags() &  ~QtCore.Qt.ItemIsEditable
+        for column in [1, 2, 3]:
+            item = QtGui.QTableWidgetItem(u'')
+            item.setFlags(flags)
+            self.tag_table_widget.setItem(row, column, item)
+        self.tag_table_widget.resizeRowsToContents()
+        self.restore_button.setEnabled(False)
+        self.save_button.setEnabled(False)
+
+    def on_current_cell_changed(self, cur_row, cur_col, pre_row, pre_col):
+        """
+        This function processes all user edits in the table. It is called on each cell change.
+        """
+        # only process for editable rows
+        pre_row_item = self.tag_table_widget.item(pre_row, 0)
+        if not pre_row_item or not (pre_row_item.flags() & QtCore.Qt.ItemIsEditable):
+            return
+        repair = False
+        data = self.item_to_data_dict(pre_row_item)
+        item = self.tag_table_widget.item(pre_row, pre_col)
+        text = unicode(item.text())
+        if pre_col is 0:
+            # Tag name edited
+            if text:
+                for row in range(self.tag_table_widget.rowCount()):
+                    counting_item = self.tag_table_widget.item(row, 0)
+                    if row != pre_row and counting_item and counting_item.text() == text:
+                        answer = QtGui.QMessageBox.warning(self,
+                            translate('OpenLP.FormattingTagForm', 'Validation Error'),
+                            translate('OpenLP.FormattingTagForm',
+                                'Tag %s is already defined. Please pick a different one.' % text),
+                            QtGui.QMessageBox.Discard|QtGui.QMessageBox.Ok)
+                        if answer == QtGui.QMessageBox.Discard:
+                            item.setText(data.get(u'tag'))
+                        else:
+                            repair = True
+                        break
+                if not repair:
+                    data[u'tag'] = text
+                    data.setdefault(u'description', u'')
+                    data.setdefault(u'html', u'')
+                    pre_row_item.setData(QtCore.Qt.UserRole, data)
+                    flags = self.tag_table_widget.item(pre_row, 1).flags()
+                    if not (flags & QtCore.Qt.ItemIsEditable):
+                        # if description cell is read only, the user is adding a new tag.
+                        #   So we add another empty row and enable editing for description and html.
+                        new_row = self.tag_table_widget.rowCount()
+                        self.tag_table_widget.insertRow(new_row)
+                        for column in range(4):
+                            new_item = QtGui.QTableWidgetItem(u'')
+                            if column != 0:
+                                new_item.setFlags(flags)
+                            self.tag_table_widget.setItem(new_row, column, new_item)
+                        for column in [1, 2]:
+                            self.tag_table_widget.item(pre_row, column).setFlags(item.flags())
+                        # trigger edit as editing might have been enabled after selecting
+                        cur_item = self.tag_table_widget.item(cur_row, cur_col)
+                        if cur_item and cur_item.flags() & ~QtCore.Qt.ItemIsEditable:
+                            self.tag_table_widget.editItem(cur_item)
+                        self.tag_table_widget.resizeRowsToContents()
+            else:
+                answer = None
+                if self.tag_table_widget.item(pre_row, 1).text() \
+                    or self.tag_table_widget.item(pre_row, 2).text():
+                    answer = QtGui.QMessageBox.warning(self,
+                        translate('OpenLP.FormattingTagForm', 'Validation Error'),
+                        translate('OpenLP.FormattingTagForm',
+                            'No tag name defined. Do you want to delete the whole tag?'),
+                        QtGui.QMessageBox.Yes|QtGui.QMessageBox.Discard|QtGui.QMessageBox.Cancel)
+                if answer == QtGui.QMessageBox.Discard:
+                    item.setText(data.get(u'tag'))
+                if answer == QtGui.QMessageBox.Cancel:
+                    repair = True
+                elif pre_row < self.tag_table_widget.rowCount() - 1:
+                    self.tag_table_widget.removeRow(pre_row)
+        elif pre_col is 1:
+            # Description edited
+            data[u'description'] = text
+            pre_row_item.setData(QtCore.Qt.UserRole, data)
+        elif pre_col is 2:
+            # HTML edited
+            end_html = self.start_html_to_end_html(text)
+            if end_html is not None:
+                item.setToolTip(cgi.escape(text))
+                if self.tag_table_widget.item(pre_row, 3) is None:
+                    self.tag_table_widget.setItem(pre_row, 3, QtGui.QTableWidgetItem(end_html))
+                else:
+                    self.tag_table_widget.item(pre_row, 3).setText(end_html)
+                self.tag_table_widget.item(pre_row, 3).setToolTip(cgi.escape(end_html))
+                data[u'html'] = text
+                pre_row_item.setData(QtCore.Qt.UserRole, data)
+                self.tag_table_widget.resizeRowsToContents()
+            elif QtGui.QMessageBox.question(self,
+                    translate('OpenLP.FormattingTagForm', 'Validation Error'),
                     translate('OpenLP.FormattingTagForm',
-                    'Tag "n" already defined.'))
-                return
-        # Add new tag to list
-        tag = {
-            u'desc': translate('OpenLP.FormattingTagForm', 'New Tag'),
-            u'start tag': u'{n}',
-            u'start html': translate('OpenLP.FormattingTagForm', '<HTML here>'),
-            u'end tag': u'{/n}',
-            u'end html': translate('OpenLP.FormattingTagForm', '</and here>'),
-            u'protected': False,
-            u'temporary': False
-        }
-        FormattingTags.add_html_tags([tag])
-        FormattingTags.save_html_tags()
-        self._reloadTable()
-        # Highlight new row
-        self.tagTableWidget.selectRow(self.tagTableWidget.rowCount() - 1)
-        self.onRowSelected()
-        self.tagTableWidget.scrollToBottom()
-        #self.savePushButton.setEnabled(False)
-
-    def onDeleteClicked(self):
-        """
-        Delete selected custom tag.
-        """
-        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.tagTableWidget.selectRow(self.selected - 1)
-            self.selected = -1
-            FormattingTags.save_html_tags()
-            self._reloadTable()
-
-    def onSavedClicked(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 = unicode(self.tagLineEdit.text())
-            for linenumber, html1 in enumerate(html_expands):
-                if self._strip(html1[u'start tag']) == tag and \
-                    linenumber != self.selected:
-                    critical_error_message_box(
-                        translate('OpenLP.FormattingTagForm', 'Update Error'),
-                        unicode(translate('OpenLP.FormattingTagForm',
-                        'Tag %s already defined.')) % tag)
-                    return
-            html[u'desc'] = unicode(self.descriptionLineEdit.text())
-            html[u'start html'] = unicode(self.startTagLineEdit.text())
-            html[u'end html'] = unicode(self.endTagLineEdit.text())
-            html[u'start tag'] = u'{%s}' % tag
-            html[u'end tag'] = u'{/%s}' % tag
-            # Keep temporary tags when the user changes one.
-            html[u'temporary'] = False
-            self.selected = -1
-        FormattingTags.save_html_tags()
-        self._reloadTable()
-
-    def _reloadTable(self):
-        """
-        Reset List for loading.
-        """
-        self.tagTableWidget.clearContents()
-        self.tagTableWidget.setRowCount(0)
-        self.newPushButton.setEnabled(True)
-        self.savePushButton.setEnabled(False)
-        self.deletePushButton.setEnabled(False)
-        for linenumber, html in enumerate(FormattingTags.get_html_tags()):
-            self.tagTableWidget.setRowCount(self.tagTableWidget.rowCount() + 1)
-            self.tagTableWidget.setItem(linenumber, 0,
-                QtGui.QTableWidgetItem(html[u'desc']))
-            self.tagTableWidget.setItem(linenumber, 1,
-                QtGui.QTableWidgetItem(self._strip(html[u'start tag'])))
-            self.tagTableWidget.setItem(linenumber, 2,
-                QtGui.QTableWidgetItem(html[u'start html']))
-            self.tagTableWidget.setItem(linenumber, 3,
-                QtGui.QTableWidgetItem(html[u'end html']))
-            # Permanent (persistent) tags do not have this key.
-            if u'temporary' not in html:
-                html[u'temporary'] = False
-            self.tagTableWidget.resizeRowsToContents()
-        self.descriptionLineEdit.setText(u'')
-        self.tagLineEdit.setText(u'')
-        self.startTagLineEdit.setText(u'')
-        self.endTagLineEdit.setText(u'')
-        self.descriptionLineEdit.setEnabled(False)
-        self.tagLineEdit.setEnabled(False)
-        self.startTagLineEdit.setEnabled(False)
-        self.endTagLineEdit.setEnabled(False)
-
-    def _strip(self, tag):
-        """
-        Remove tag wrappers for editing.
-        """
-        tag = tag.replace(u'{', u'')
-        tag = tag.replace(u'}', u'')
-        return tag
+                        'The entered HTML is not valid. Please enter valid HTML.'),
+                    QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel) == QtGui.QMessageBox.Cancel:
+                item.setText(data.get(u'html'))
+            else:
+                repair = True
+        if repair:
+            self.tag_table_widget.setCurrentItem(item, QtGui.QItemSelectionModel.ClearAndSelect)
+            widget = self.tag_table_widget.cellWidget(pre_row, pre_col)
+            if widget:
+                widget.setFocus()
+
+    def on_data_change(self, item):
+        self.restore_button.setEnabled(True)
+        self.save_button.setEnabled(True)
+
+    def start_html_to_end_html(self, start_html):
+        """
+        Return the end HTML for a given start HTML or None if invalid.
+        """
+        end_tags = []
+        tag_regex = re.compile(r'<(?:(?P<close>/(?=[^\s/>]+>))?'
+            r'(?P<tag>[^\s/!\?>]+)(?:\s+[^\s=]+="[^"]*")*\s*(?P<empty>/)?'
+            r'|(?P<cdata>!\[CDATA\[(?:(?!\]\]>).)*\]\])'
+            r'|(?P<procinst>\?(?:(?!\?>).)*\?)'
+            r'|(?P<comment>!--(?:(?!-->).)*--))>', re.UNICODE)
+        code_regex = re.compile(r'^(?:[^<>]*%s)*[^<>]*$' % tag_regex.pattern)
+        match = code_regex.match(start_html)
+        if match:
+            match = tag_regex.search(start_html)
+            while match:
+                if match.group(u'tag'):
+                    tag = match.group(u'tag').lower()
+                    if match.group(u'close'):
+                        if match.group(u'empty') or end_tags.pop() != tag:
+                            return
+                    elif not match.group(u'empty'):
+                        end_tags.append(tag)
+                match = tag_regex.search(start_html, match.end())
+            return u''.join(map(lambda tag: u'</%s>' % tag, reversed(end_tags)))
+
+    def item_to_data_dict(self, item):
+        """
+        Reads the data from a QTableWidgetItem and returns a dict with unicode keys and values.
+        """
+        data = {}
+        for key, value in item.data(QtCore.Qt.UserRole).toMap().items():
+            data[unicode(key)] = unicode(value.toString())
+        return data
+
+    def save_tags(self):
+        """
+        Saves the table to the settings.
+        """
+        for id, tag in enumerate(FormattingTags.get_html_tags()):
+            if not tag.get(u'protected'):
+                FormattingTags.remove_html_tag(id)
+        tags = []
+        for row in range(self.tag_table_widget.rowCount()):
+            if self.tag_table_widget.item(row, 0).flags() & QtCore.Qt.ItemIsEditable:
+                data = self.item_to_data_dict(self.tag_table_widget.item(row, 0))
+                if data.get(u'tag'):
+                    tags.append({
+                        u'desc': data[u'description'],
+                        u'start tag': u'{%s}' % data[u'tag'],
+                        u'start html': data[u'html'],
+                        u'end tag': u'{/%s}' % data[u'tag'],
+                        u'end html': self.start_html_to_end_html(data[u'html']),
+                        u'protected': False,
+                        u'temporary': False})
+        FormattingTags.add_html_tags(tags)
+        FormattingTags.save_html_tags()