From da91313aaedbc31434a2a17fa3710dc3328b5494 Mon Sep 17 00:00:00 2001 From: Eli Algranti Date: Tue, 10 Apr 2012 11:35:39 +1000 Subject: [PATCH 1/8] UI changes only: Added list of search-replace items. --- src/calibre/gui2/convert/regex_builder.py | 3 ++ .../gui2/convert/search_and_replace.py | 52 +++++++++++++++++- .../gui2/convert/search_and_replace.ui | 53 +++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/convert/regex_builder.py b/src/calibre/gui2/convert/regex_builder.py index bbbef7e741..0a07e92e25 100644 --- a/src/calibre/gui2/convert/regex_builder.py +++ b/src/calibre/gui2/convert/regex_builder.py @@ -226,6 +226,9 @@ class RegexEdit(QWidget, Ui_Edit): def set_doc(self, doc): self.doc_cache = doc + def set_regex(self, regex): + self.edit.setText(regex) + def break_cycles(self): self.db = self.doc_cache = None diff --git a/src/calibre/gui2/convert/search_and_replace.py b/src/calibre/gui2/convert/search_and_replace.py index b9e2644008..833b80b320 100644 --- a/src/calibre/gui2/convert/search_and_replace.py +++ b/src/calibre/gui2/convert/search_and_replace.py @@ -6,6 +6,8 @@ __docformat__ = 'restructuredtext en' import re +from PyQt4.QtCore import SIGNAL, Qt +from PyQt4.QtGui import QTableWidgetItem from calibre.gui2.convert.search_and_replace_ui import Ui_Form from calibre.gui2.convert import Widget from calibre.gui2 import error_dialog @@ -29,6 +31,7 @@ class SearchAndReplaceWidget(Widget, Ui_Form): self.opt_sr1_search.set_msg(_('&Search Regular Expression')) self.opt_sr1_search.set_book_id(book_id) self.opt_sr1_search.set_db(db) + self.opt_sr1_search.set_regex('test.*') self.opt_sr2_search.set_msg(_('&Search Regular Expression')) self.opt_sr2_search.set_book_id(book_id) self.opt_sr2_search.set_db(db) @@ -40,6 +43,49 @@ class SearchAndReplaceWidget(Widget, Ui_Form): self.opt_sr2_search.doc_update.connect(self.update_doc) self.opt_sr3_search.doc_update.connect(self.update_doc) + self.opt_sr.setColumnCount(2) + self.opt_sr.setHorizontalHeaderLabels(['Search Expression', 'Replacement']) + self.connect(self.sr_add, SIGNAL('clicked()'), self.sr_add_clicked) + self.connect(self.sr_change, SIGNAL('clicked()'), self.sr_change_clicked) + self.connect(self.sr_remove, SIGNAL('clicked()'), self.sr_remove_clicked) + self.connect(self.opt_sr, SIGNAL('currentCellChanged(int, int, int, int)'), self.sr_currentCellChanged) + + def sr_add_clicked(self): + if self.opt_sr1_search.regex: + self.opt_sr.insertRow(0) + newItem = QTableWidgetItem() + newItem.setFlags(Qt.ItemFlags(Qt.ItemIsSelectable + Qt.ItemIsEnabled)) + newItem.setText(self.opt_sr1_search.regex) + self.opt_sr.setItem(0,0, newItem) + newItem = QTableWidgetItem() + newItem.setFlags(Qt.ItemFlags(Qt.ItemIsSelectable + Qt.ItemIsEnabled)) + newItem.setText(self.opt_sr1_replace.text()) + self.opt_sr.setItem(0,1, newItem) + self.opt_sr.setCurrentCell(0, 0) + + def sr_change_clicked(self): + row = self.opt_sr.currentRow() + if row >= 0: + self.opt_sr.item(row, 0).setText(self.opt_sr1_search.regex) + self.opt_sr.item(row, 1).setText(self.opt_sr1_replace.text()) + self.opt_sr.setCurrentCell(row, 0) + + def sr_remove_clicked(self): + row = self.opt_sr.currentRow() + if row >= 0: + self.opt_sr.removeRow(row) + self.opt_sr.setCurrentCell(row-1, 0) + + def sr_currentCellChanged(self, row, column, previousRow, previousColumn) : + if row >= 0: + self.sr_change.setEnabled(True) + self.sr_remove.setEnabled(True) + self.opt_sr1_search.set_regex(self.opt_sr.item(row, 0).text()) + self.opt_sr1_replace.setText(self.opt_sr.item(row, 1).text()) + else: + self.sr_change.setEnabled(False) + self.sr_remove.setEnabled(False) + def break_cycles(self): Widget.break_cycles(self) @@ -74,4 +120,8 @@ class SearchAndReplaceWidget(Widget, Ui_Form): return False return True - + def opt_sr_items + items = [] + for row in xrange(0, self.opt_sr.rowCount()): + items.append([self.opt_sr.getItem(row,0).text(), self.opt_sr.getItem(row,1).text()]) + return items diff --git a/src/calibre/gui2/convert/search_and_replace.ui b/src/calibre/gui2/convert/search_and_replace.ui index 03a74b5ebd..d55b5cd0a6 100644 --- a/src/calibre/gui2/convert/search_and_replace.ui +++ b/src/calibre/gui2/convert/search_and_replace.ui @@ -185,6 +185,59 @@ + + + + -1 + + + 0 + + + + + Add + + + + + + + Change + + + false + + + + + + + Remove + + + false + + + + + + + + + + 0 + 0 + + + + QAbstractItemView::SelectRows + + + QAbstractItemView::SingleSelection + + + From 07648f1ff3628dd4a70bdbabe61a3838197adc7b Mon Sep 17 00:00:00 2001 From: Eli Algranti Date: Tue, 10 Apr 2012 12:13:04 +1000 Subject: [PATCH 2/8] Fixed syntax error in opt_sr_items declaration --- src/calibre/gui2/convert/search_and_replace.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/convert/search_and_replace.py b/src/calibre/gui2/convert/search_and_replace.py index 833b80b320..4e0bfdf020 100644 --- a/src/calibre/gui2/convert/search_and_replace.py +++ b/src/calibre/gui2/convert/search_and_replace.py @@ -120,7 +120,8 @@ class SearchAndReplaceWidget(Widget, Ui_Form): return False return True - def opt_sr_items + @property + def opt_sr_items(self): items = [] for row in xrange(0, self.opt_sr.rowCount()): items.append([self.opt_sr.getItem(row,0).text(), self.opt_sr.getItem(row,1).text()]) From 9bf3bcb75d7cd32c55df9ebc8c7c0bff27528b88 Mon Sep 17 00:00:00 2001 From: Eli Algranti Date: Tue, 10 Apr 2012 16:19:23 +1000 Subject: [PATCH 3/8] Added load/save for search replace definitions and plumbing to get the search/replace to work during conversion --- src/calibre/ebooks/conversion/cli.py | 4 +- src/calibre/ebooks/conversion/plumber.py | 29 +--- src/calibre/ebooks/conversion/preprocess.py | 8 +- src/calibre/gui2/convert/__init__.py | 30 +++- .../gui2/convert/search_and_replace.py | 115 ++++++++------- .../gui2/convert/search_and_replace.ui | 132 +++--------------- 6 files changed, 111 insertions(+), 207 deletions(-) diff --git a/src/calibre/ebooks/conversion/cli.py b/src/calibre/ebooks/conversion/cli.py index 559402ca1c..a80ddfb839 100644 --- a/src/calibre/ebooks/conversion/cli.py +++ b/src/calibre/ebooks/conversion/cli.py @@ -156,9 +156,7 @@ def add_pipeline_options(parser, plumber): 'SEARCH AND REPLACE' : ( _('Modify the document text and structure using user defined patterns.'), [ - 'sr1_search', 'sr1_replace', - 'sr2_search', 'sr2_replace', - 'sr3_search', 'sr3_replace', + 'search_replace', ] ), diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 8bb4fdd891..62abc30fbd 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -600,32 +600,9 @@ OptionRecommendation(name='renumber_headings', 'The tags are renumbered to prevent splitting in the middle ' 'of chapter headings.')), -OptionRecommendation(name='sr1_search', - recommended_value='', level=OptionRecommendation.LOW, - help=_('Search pattern (regular expression) to be replaced with ' - 'sr1-replace.')), - -OptionRecommendation(name='sr1_replace', - recommended_value='', level=OptionRecommendation.LOW, - help=_('Replacement to replace the text found with sr1-search.')), - -OptionRecommendation(name='sr2_search', - recommended_value='', level=OptionRecommendation.LOW, - help=_('Search pattern (regular expression) to be replaced with ' - 'sr2-replace.')), - -OptionRecommendation(name='sr2_replace', - recommended_value='', level=OptionRecommendation.LOW, - help=_('Replacement to replace the text found with sr2-search.')), - -OptionRecommendation(name='sr3_search', - recommended_value='', level=OptionRecommendation.LOW, - help=_('Search pattern (regular expression) to be replaced with ' - 'sr3-replace.')), - -OptionRecommendation(name='sr3_replace', - recommended_value='', level=OptionRecommendation.LOW, - help=_('Replacement to replace the text found with sr3-search.')), +OptionRecommendation(name='search_replace', + recommended_value='[]', level=OptionRecommendation.LOW, + help=_('Modify the document text and structure using user defined patterns.')), ] # }}} diff --git a/src/calibre/ebooks/conversion/preprocess.py b/src/calibre/ebooks/conversion/preprocess.py index 617de18555..f232cd3a92 100644 --- a/src/calibre/ebooks/conversion/preprocess.py +++ b/src/calibre/ebooks/conversion/preprocess.py @@ -5,7 +5,7 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import functools, re +import functools, re, json from calibre import entity_to_unicode, as_unicode @@ -515,14 +515,14 @@ class HTMLPreProcessor(object): if not getattr(self.extra_opts, 'keep_ligatures', False): html = _ligpat.sub(lambda m:LIGATURES[m.group()], html) - for search, replace in [['sr3_search', 'sr3_replace'], ['sr2_search', 'sr2_replace'], ['sr1_search', 'sr1_replace']]: - search_pattern = getattr(self.extra_opts, search, '') + search_replace = json.loads(getattr(self.extra_opts, 'search_replace', '[]')) + for search_pattern, replace_txt in search_replace: if search_pattern: try: search_re = re.compile(search_pattern) - replace_txt = getattr(self.extra_opts, replace, '') if not replace_txt: replace_txt = '' + print 'Replacing pattern \'{0}\' with text \'{1}\''.format(search_pattern, replace_txt) rules.insert(0, (search_re, replace_txt)) except Exception as e: self.log.error('Failed to parse %r regexp because %s' % diff --git a/src/calibre/gui2/convert/__init__.py b/src/calibre/gui2/convert/__init__.py index 73b478ac47..fe5a6330a5 100644 --- a/src/calibre/gui2/convert/__init__.py +++ b/src/calibre/gui2/convert/__init__.py @@ -10,7 +10,7 @@ import textwrap, codecs, importlib from functools import partial from PyQt4.Qt import QWidget, QSpinBox, QDoubleSpinBox, QLineEdit, QTextEdit, \ - QCheckBox, QComboBox, Qt, QIcon, pyqtSignal, QLabel + QCheckBox, QComboBox, Qt, QIcon, pyqtSignal, QLabel, QTableWidget from calibre.customize.conversion import OptionRecommendation from calibre.ebooks.conversion.config import load_defaults, \ @@ -160,6 +160,15 @@ class Widget(QWidget): return g.xpath if g.xpath else None elif isinstance(g, RegexEdit): return g.regex if g.regex else None + elif isinstance(g, QTableWidget): + import json + ans = [] + for row in xrange(0, g.rowCount()): + colItems = [] + for col in xrange(0, g.columnCount()): + colItems.append(unicode(g.item(row, col).text())) + ans.append(colItems) + return json.dumps(ans) else: raise Exception('Can\'t get value from %s'%type(g)) @@ -187,6 +196,8 @@ class Widget(QWidget): elif isinstance(g, (XPathEdit, RegexEdit)): g.edit.editTextChanged.connect(f) g.edit.currentIndexChanged.connect(f) + elif isinstance(g, QTableWidget): + g.cellChanged.connect(f) else: raise Exception('Can\'t connect %s'%type(g)) @@ -220,6 +231,23 @@ class Widget(QWidget): g.setCheckState(Qt.Checked if bool(val) else Qt.Unchecked) elif isinstance(g, (XPathEdit, RegexEdit)): g.edit.setText(val if val else '') + elif isinstance(g, (QTableWidget)): + import json + try: + rowItems = json.loads(val) + if not isinstance(rowItems, list): + rowItems = [] + except: + rowItems = [] + + + g.setRowCount(len(rowItems)) + + for row, colItems in enumerate(rowItems): + for col, cellValue in enumerate(colItems): + newItem = g.itemPrototype().clone() + newItem.setText(cellValue) + g.setItem(row,col, newItem) else: raise Exception('Can\'t set value %s in %s'%(repr(val), unicode(g.objectName()))) diff --git a/src/calibre/gui2/convert/search_and_replace.py b/src/calibre/gui2/convert/search_and_replace.py index 4e0bfdf020..710c4719a8 100644 --- a/src/calibre/gui2/convert/search_and_replace.py +++ b/src/calibre/gui2/convert/search_and_replace.py @@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en' import re from PyQt4.QtCore import SIGNAL, Qt -from PyQt4.QtGui import QTableWidgetItem +from PyQt4.QtGui import QTableWidgetItem, QFileDialog from calibre.gui2.convert.search_and_replace_ui import Ui_Form from calibre.gui2.convert import Widget from calibre.gui2 import error_dialog @@ -22,66 +22,75 @@ class SearchAndReplaceWidget(Widget, Ui_Form): def __init__(self, parent, get_option, get_help, db=None, book_id=None): Widget.__init__(self, parent, - ['sr1_search', 'sr1_replace', - 'sr2_search', 'sr2_replace', - 'sr3_search', 'sr3_replace'] + ['search_replace'] ) self.db, self.book_id = db, book_id - self.initialize_options(get_option, get_help, db, book_id) - self.opt_sr1_search.set_msg(_('&Search Regular Expression')) - self.opt_sr1_search.set_book_id(book_id) - self.opt_sr1_search.set_db(db) - self.opt_sr1_search.set_regex('test.*') - self.opt_sr2_search.set_msg(_('&Search Regular Expression')) - self.opt_sr2_search.set_book_id(book_id) - self.opt_sr2_search.set_db(db) - self.opt_sr3_search.set_msg(_('&Search Regular Expression')) - self.opt_sr3_search.set_book_id(book_id) - self.opt_sr3_search.set_db(db) - self.opt_sr1_search.doc_update.connect(self.update_doc) - self.opt_sr2_search.doc_update.connect(self.update_doc) - self.opt_sr3_search.doc_update.connect(self.update_doc) + self.sr_search.set_msg(_('&Search Regular Expression')) + self.sr_search.set_book_id(book_id) + self.sr_search.set_db(db) + + self.sr_search.doc_update.connect(self.update_doc) + + proto = QTableWidgetItem() + proto.setFlags(Qt.ItemFlags(Qt.ItemIsSelectable + Qt.ItemIsEnabled)) + self.opt_search_replace.setItemPrototype(proto) + self.opt_search_replace.setColumnCount(2) + self.opt_search_replace.setHorizontalHeaderLabels(['Search Expression', 'Replacement']) - self.opt_sr.setColumnCount(2) - self.opt_sr.setHorizontalHeaderLabels(['Search Expression', 'Replacement']) self.connect(self.sr_add, SIGNAL('clicked()'), self.sr_add_clicked) self.connect(self.sr_change, SIGNAL('clicked()'), self.sr_change_clicked) self.connect(self.sr_remove, SIGNAL('clicked()'), self.sr_remove_clicked) - self.connect(self.opt_sr, SIGNAL('currentCellChanged(int, int, int, int)'), self.sr_currentCellChanged) + self.connect(self.sr_load, SIGNAL('clicked()'), self.sr_load_clicked) + self.connect(self.sr_save, SIGNAL('clicked()'), self.sr_save_clicked) + self.connect(self.opt_search_replace, SIGNAL('currentCellChanged(int, int, int, int)'), self.sr_currentCellChanged) + + self.initialize_options(get_option, get_help, db, book_id) def sr_add_clicked(self): - if self.opt_sr1_search.regex: - self.opt_sr.insertRow(0) - newItem = QTableWidgetItem() - newItem.setFlags(Qt.ItemFlags(Qt.ItemIsSelectable + Qt.ItemIsEnabled)) - newItem.setText(self.opt_sr1_search.regex) - self.opt_sr.setItem(0,0, newItem) - newItem = QTableWidgetItem() - newItem.setFlags(Qt.ItemFlags(Qt.ItemIsSelectable + Qt.ItemIsEnabled)) - newItem.setText(self.opt_sr1_replace.text()) - self.opt_sr.setItem(0,1, newItem) - self.opt_sr.setCurrentCell(0, 0) + if self.sr_search.regex: + self.opt_search_replace.insertRow(0) + newItem = self.opt_search_replace.itemPrototype().clone() + newItem.setText(self.sr_search.regex) + self.opt_search_replace.setItem(0,0, newItem) + newItem = self.opt_search_replace.itemPrototype().clone() + newItem.setText(self.sr_replace.text()) + self.opt_search_replace.setItem(0,1, newItem) + self.opt_search_replace.setCurrentCell(0, 0) def sr_change_clicked(self): - row = self.opt_sr.currentRow() + row = self.opt_search_replace.currentRow() if row >= 0: - self.opt_sr.item(row, 0).setText(self.opt_sr1_search.regex) - self.opt_sr.item(row, 1).setText(self.opt_sr1_replace.text()) - self.opt_sr.setCurrentCell(row, 0) + self.opt_search_replace.item(row, 0).setText(self.sr_search.regex) + self.opt_search_replace.item(row, 1).setText(self.sr_replace.text()) + self.opt_search_replace.setCurrentCell(row, 0) def sr_remove_clicked(self): - row = self.opt_sr.currentRow() + row = self.opt_search_replace.currentRow() if row >= 0: - self.opt_sr.removeRow(row) - self.opt_sr.setCurrentCell(row-1, 0) + self.opt_search_replace.removeRow(row) + self.opt_search_replace.setCurrentCell(row-1, 0) + + def sr_load_clicked(self): + filename = QFileDialog.getOpenFileName(self, 'Load Calibre Search-Replace definitions file', '.', 'Calibre Search-Replace definitions file (*.csr)') + if filename: + with open(filename, 'r') as f: + val = f.read() + self.set_value(self.opt_search_replace, val) + + def sr_save_clicked(self): + filename = QFileDialog.getSaveFileName(self, 'Save Calibre Search-Replace definitions file', '.', 'Calibre Search-Replace definitions file (*.csr)') + if filename: + with open(filename, 'w') as f: + val = self.get_value(self.opt_search_replace) + f.write(val) def sr_currentCellChanged(self, row, column, previousRow, previousColumn) : if row >= 0: self.sr_change.setEnabled(True) self.sr_remove.setEnabled(True) - self.opt_sr1_search.set_regex(self.opt_sr.item(row, 0).text()) - self.opt_sr1_replace.setText(self.opt_sr.item(row, 1).text()) + self.sr_search.set_regex(self.opt_search_replace.item(row, 0).text()) + self.sr_replace.setText(self.opt_search_replace.item(row, 1).text()) else: self.sr_change.setEnabled(False) self.sr_remove.setEnabled(False) @@ -95,34 +104,20 @@ class SearchAndReplaceWidget(Widget, Ui_Form): except: pass - d(self.opt_sr1_search) - d(self.opt_sr2_search) - d(self.opt_sr3_search) + d(self.sr_search) - self.opt_sr1_search.break_cycles() - self.opt_sr2_search.break_cycles() - self.opt_sr3_search.break_cycles() + self.sr_search.break_cycles() def update_doc(self, doc): - self.opt_sr1_search.set_doc(doc) - self.opt_sr2_search.set_doc(doc) - self.opt_sr3_search.set_doc(doc) + self.sr_search.set_doc(doc) def pre_commit_check(self): - for x in ('sr1_search', 'sr2_search', 'sr3_search'): - x = getattr(self, 'opt_'+x) + for row in xrange(0, self.opt_search_replace.rowCount()): try: - pat = unicode(x.regex) + pat = unicode(self.opt_search_replace.item(row,0).text()) re.compile(pat) except Exception as err: error_dialog(self, _('Invalid regular expression'), _('Invalid regular expression: %s')%err, show=True) return False return True - - @property - def opt_sr_items(self): - items = [] - for row in xrange(0, self.opt_sr.rowCount()): - items.append([self.opt_sr.getItem(row,0).text(), self.opt_sr.getItem(row,1).text()]) - return items diff --git a/src/calibre/gui2/convert/search_and_replace.ui b/src/calibre/gui2/convert/search_and_replace.ui index d55b5cd0a6..af276d7992 100644 --- a/src/calibre/gui2/convert/search_and_replace.ui +++ b/src/calibre/gui2/convert/search_and_replace.ui @@ -39,7 +39,7 @@ QLayout::SetMinimumSize - + 0 @@ -60,12 +60,12 @@ &Replacement Text - opt_sr1_replace + sr_replace - + 0 @@ -78,114 +78,6 @@ - - - - 0 - 0 - - - - Second Expression - - - - QLayout::SetMinimumSize - - - - - - 0 - 0 - - - - - - - - - 0 - 0 - - - - &Replacement Text - - - opt_sr2_replace - - - - - - - - 0 - 0 - - - - - - - - - - - - 0 - 0 - - - - Third expression - - - - QLayout::SetMinimumSize - - - - - - 0 - 0 - - - - - - - - - 0 - 0 - - - - &Replacement Text - - - opt_sr3_replace - - - - - - - - 0 - 0 - - - - - - - - -1 @@ -220,10 +112,24 @@ + + + + Load + + + + + + + Save + + + - - + + 0 From 9fc6dc68787f3b6b68722ccf05b361204e0f8169 Mon Sep 17 00:00:00 2001 From: Eli Algranti Date: Wed, 11 Apr 2012 09:55:02 +1000 Subject: [PATCH 4/8] Set column size --- src/calibre/gui2/convert/search_and_replace.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/gui2/convert/search_and_replace.py b/src/calibre/gui2/convert/search_and_replace.py index 710c4719a8..900f74804f 100644 --- a/src/calibre/gui2/convert/search_and_replace.py +++ b/src/calibre/gui2/convert/search_and_replace.py @@ -36,6 +36,8 @@ class SearchAndReplaceWidget(Widget, Ui_Form): proto.setFlags(Qt.ItemFlags(Qt.ItemIsSelectable + Qt.ItemIsEnabled)) self.opt_search_replace.setItemPrototype(proto) self.opt_search_replace.setColumnCount(2) + self.opt_search_replace.setColumnWidth(0, 300) + self.opt_search_replace.setColumnWidth(1, 300) self.opt_search_replace.setHorizontalHeaderLabels(['Search Expression', 'Replacement']) self.connect(self.sr_add, SIGNAL('clicked()'), self.sr_add_clicked) From 051d7c71c200ba5032944a9cfa0a0c1c9fea5bce Mon Sep 17 00:00:00 2001 From: Eli Algranti Date: Thu, 12 Apr 2012 09:35:09 +1000 Subject: [PATCH 5/8] Reverted Widget in __init__.py and moved functionality to search_and_replace.py by subclassing set_value_handler, get_value_handler and connect_gui_obj_handler --- src/calibre/gui2/convert/__init__.py | 30 +-------------- .../gui2/convert/search_and_replace.py | 37 ++++++++++++++++++- 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/src/calibre/gui2/convert/__init__.py b/src/calibre/gui2/convert/__init__.py index fe5a6330a5..73b478ac47 100644 --- a/src/calibre/gui2/convert/__init__.py +++ b/src/calibre/gui2/convert/__init__.py @@ -10,7 +10,7 @@ import textwrap, codecs, importlib from functools import partial from PyQt4.Qt import QWidget, QSpinBox, QDoubleSpinBox, QLineEdit, QTextEdit, \ - QCheckBox, QComboBox, Qt, QIcon, pyqtSignal, QLabel, QTableWidget + QCheckBox, QComboBox, Qt, QIcon, pyqtSignal, QLabel from calibre.customize.conversion import OptionRecommendation from calibre.ebooks.conversion.config import load_defaults, \ @@ -160,15 +160,6 @@ class Widget(QWidget): return g.xpath if g.xpath else None elif isinstance(g, RegexEdit): return g.regex if g.regex else None - elif isinstance(g, QTableWidget): - import json - ans = [] - for row in xrange(0, g.rowCount()): - colItems = [] - for col in xrange(0, g.columnCount()): - colItems.append(unicode(g.item(row, col).text())) - ans.append(colItems) - return json.dumps(ans) else: raise Exception('Can\'t get value from %s'%type(g)) @@ -196,8 +187,6 @@ class Widget(QWidget): elif isinstance(g, (XPathEdit, RegexEdit)): g.edit.editTextChanged.connect(f) g.edit.currentIndexChanged.connect(f) - elif isinstance(g, QTableWidget): - g.cellChanged.connect(f) else: raise Exception('Can\'t connect %s'%type(g)) @@ -231,23 +220,6 @@ class Widget(QWidget): g.setCheckState(Qt.Checked if bool(val) else Qt.Unchecked) elif isinstance(g, (XPathEdit, RegexEdit)): g.edit.setText(val if val else '') - elif isinstance(g, (QTableWidget)): - import json - try: - rowItems = json.loads(val) - if not isinstance(rowItems, list): - rowItems = [] - except: - rowItems = [] - - - g.setRowCount(len(rowItems)) - - for row, colItems in enumerate(rowItems): - for col, cellValue in enumerate(colItems): - newItem = g.itemPrototype().clone() - newItem.setText(cellValue) - g.setItem(row,col, newItem) else: raise Exception('Can\'t set value %s in %s'%(repr(val), unicode(g.objectName()))) diff --git a/src/calibre/gui2/convert/search_and_replace.py b/src/calibre/gui2/convert/search_and_replace.py index 900f74804f..d0ad43e1b4 100644 --- a/src/calibre/gui2/convert/search_and_replace.py +++ b/src/calibre/gui2/convert/search_and_replace.py @@ -4,10 +4,10 @@ __license__ = 'GPL 3' __copyright__ = '2011, John Schember ' __docformat__ = 'restructuredtext en' -import re +import re, json from PyQt4.QtCore import SIGNAL, Qt -from PyQt4.QtGui import QTableWidgetItem, QFileDialog +from PyQt4.QtGui import QTableWidget, QTableWidgetItem, QFileDialog from calibre.gui2.convert.search_and_replace_ui import Ui_Form from calibre.gui2.convert import Widget from calibre.gui2 import error_dialog @@ -123,3 +123,36 @@ class SearchAndReplaceWidget(Widget, Ui_Form): _('Invalid regular expression: %s')%err, show=True) return False return True + + + # Options handling + + def connect_gui_obj_handler(self, g, slot): + if isinstance(g, QTableWidget): + g.cellChanged.connect(slot) + + def get_value_handler(self, g): + ans = [] + for row in xrange(0, g.rowCount()): + colItems = [] + for col in xrange(0, g.columnCount()): + colItems.append(unicode(g.item(row, col).text())) + ans.append(colItems) + return json.dumps(ans) + + def set_value_handler(self, g, val): + try: + rowItems = json.loads(val) + if not isinstance(rowItems, list): + rowItems = [] + except: + rowItems = [] + + g.setRowCount(len(rowItems)) + + for row, colItems in enumerate(rowItems): + for col, cellValue in enumerate(colItems): + newItem = g.itemPrototype().clone() + newItem.setText(cellValue) + g.setItem(row,col, newItem) + return True From 298654d9fa7d42b0ec40fd334079a00e0edbb002 Mon Sep 17 00:00:00 2001 From: Eli Algranti Date: Thu, 12 Apr 2012 13:17:32 +1000 Subject: [PATCH 6/8] Added setup_help_handler to complement all the other handlers (set_value_handler, get_value_handler, connect_gui_obj_handler) This allows adding options not tied to a widget without having to change Widget --- src/calibre/gui2/convert/__init__.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/calibre/gui2/convert/__init__.py b/src/calibre/gui2/convert/__init__.py index 73b478ac47..7aee392d01 100644 --- a/src/calibre/gui2/convert/__init__.py +++ b/src/calibre/gui2/convert/__init__.py @@ -233,19 +233,22 @@ class Widget(QWidget): pass def setup_help(self, help_provider): - w = textwrap.TextWrapper(80) for name in self._options: g = getattr(self, 'opt_'+name, None) if g is None: continue help = help_provider(name) if not help: continue + if self.setup_help_handler(g, help): continue g._help = help - htext = u'
%s
'%prepare_string_for_xml( - '\n'.join(w.wrap(help))) - g.setToolTip(htext) - g.setWhatsThis(htext) - g.__class__.enterEvent = lambda obj, event: self.set_help(getattr(obj, '_help', obj.toolTip())) + self.setup_widget_help(g) + + def setup_widget_help(self, g): + w = textwrap.TextWrapper(80) + htext = u'
%s
'%prepare_string_for_xml('\n'.join(w.wrap(g._help))) + g.setToolTip(htext) + g.setWhatsThis(htext) + g.__class__.enterEvent = lambda obj, event: self.set_help(getattr(obj, '_help', obj.toolTip())) def set_value_handler(self, g, val): @@ -260,6 +263,9 @@ class Widget(QWidget): def post_get_value(self, g): pass + def setup_help_handler(self, g, help): + return False + def break_cycles(self): self.db = None From 7ea167eec1915428a6960f1f3caddb20de75e68e Mon Sep 17 00:00:00 2001 From: Eli Algranti Date: Thu, 12 Apr 2012 13:18:43 +1000 Subject: [PATCH 7/8] Returned all the original options sr?_search and sr?_replace and added backward compatibility to search_and_replace.py --- src/calibre/ebooks/conversion/cli.py | 5 +- src/calibre/ebooks/conversion/plumber.py | 26 +++ .../gui2/convert/search_and_replace.py | 150 +++++++++++++----- .../gui2/convert/search_and_replace.ui | 2 +- 4 files changed, 145 insertions(+), 38 deletions(-) diff --git a/src/calibre/ebooks/conversion/cli.py b/src/calibre/ebooks/conversion/cli.py index a80ddfb839..92f161bb33 100644 --- a/src/calibre/ebooks/conversion/cli.py +++ b/src/calibre/ebooks/conversion/cli.py @@ -156,7 +156,10 @@ def add_pipeline_options(parser, plumber): 'SEARCH AND REPLACE' : ( _('Modify the document text and structure using user defined patterns.'), [ - 'search_replace', + 'sr1_search', 'sr1_replace', + 'sr2_search', 'sr2_replace', + 'sr3_search', 'sr3_replace', + 'search_replace', ] ), diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 62abc30fbd..88e56c4633 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -599,6 +599,32 @@ OptionRecommendation(name='renumber_headings', help=_('Looks for occurrences of sequential

or

tags. ' 'The tags are renumbered to prevent splitting in the middle ' 'of chapter headings.')), +OptionRecommendation(name='sr1_search', + recommended_value='', level=OptionRecommendation.LOW, + help=_('Search pattern (regular expression) to be replaced with ' + 'sr1-replace.')), + +OptionRecommendation(name='sr1_replace', + recommended_value='', level=OptionRecommendation.LOW, + help=_('Replacement to replace the text found with sr1-search.')), + +OptionRecommendation(name='sr2_search', + recommended_value='', level=OptionRecommendation.LOW, + help=_('Search pattern (regular expression) to be replaced with ' + 'sr2-replace.')), + +OptionRecommendation(name='sr2_replace', + recommended_value='', level=OptionRecommendation.LOW, + help=_('Replacement to replace the text found with sr2-search.')), + +OptionRecommendation(name='sr3_search', + recommended_value='', level=OptionRecommendation.LOW, + help=_('Search pattern (regular expression) to be replaced with ' + 'sr3-replace.')), + +OptionRecommendation(name='sr3_replace', + recommended_value='', level=OptionRecommendation.LOW, + help=_('Replacement to replace the text found with sr3-search.')), OptionRecommendation(name='search_replace', recommended_value='[]', level=OptionRecommendation.LOW, diff --git a/src/calibre/gui2/convert/search_and_replace.py b/src/calibre/gui2/convert/search_and_replace.py index d0ad43e1b4..7389482450 100644 --- a/src/calibre/gui2/convert/search_and_replace.py +++ b/src/calibre/gui2/convert/search_and_replace.py @@ -22,7 +22,10 @@ class SearchAndReplaceWidget(Widget, Ui_Form): def __init__(self, parent, get_option, get_help, db=None, book_id=None): Widget.__init__(self, parent, - ['search_replace'] + ['search_replace', + 'sr1_search', 'sr1_replace', + 'sr2_search', 'sr2_replace', + 'sr3_search', 'sr3_replace'] ) self.db, self.book_id = db, book_id @@ -34,65 +37,70 @@ class SearchAndReplaceWidget(Widget, Ui_Form): proto = QTableWidgetItem() proto.setFlags(Qt.ItemFlags(Qt.ItemIsSelectable + Qt.ItemIsEnabled)) - self.opt_search_replace.setItemPrototype(proto) - self.opt_search_replace.setColumnCount(2) - self.opt_search_replace.setColumnWidth(0, 300) - self.opt_search_replace.setColumnWidth(1, 300) - self.opt_search_replace.setHorizontalHeaderLabels(['Search Expression', 'Replacement']) + self.search_replace.setItemPrototype(proto) + self.search_replace.setColumnCount(2) + self.search_replace.setColumnWidth(0, 300) + self.search_replace.setColumnWidth(1, 300) + self.search_replace.setHorizontalHeaderLabels(['Search Expression', 'Replacement']) self.connect(self.sr_add, SIGNAL('clicked()'), self.sr_add_clicked) self.connect(self.sr_change, SIGNAL('clicked()'), self.sr_change_clicked) self.connect(self.sr_remove, SIGNAL('clicked()'), self.sr_remove_clicked) self.connect(self.sr_load, SIGNAL('clicked()'), self.sr_load_clicked) self.connect(self.sr_save, SIGNAL('clicked()'), self.sr_save_clicked) - self.connect(self.opt_search_replace, SIGNAL('currentCellChanged(int, int, int, int)'), self.sr_currentCellChanged) + self.connect(self.search_replace, SIGNAL('currentCellChanged(int, int, int, int)'), self.sr_currentCellChanged) self.initialize_options(get_option, get_help, db, book_id) def sr_add_clicked(self): if self.sr_search.regex: - self.opt_search_replace.insertRow(0) - newItem = self.opt_search_replace.itemPrototype().clone() - newItem.setText(self.sr_search.regex) - self.opt_search_replace.setItem(0,0, newItem) - newItem = self.opt_search_replace.itemPrototype().clone() - newItem.setText(self.sr_replace.text()) - self.opt_search_replace.setItem(0,1, newItem) - self.opt_search_replace.setCurrentCell(0, 0) + row = self.sr_add_row(self.sr_search.regex, self.sr_replace.text()) + self.search_replace.setCurrentCell(row, 0) + + def sr_add_row(self, search, replace): + row = self.search_replace.rowCount() + self.search_replace.setRowCount(row + 1) + newItem = self.search_replace.itemPrototype().clone() + newItem.setText(search) + self.search_replace.setItem(row,0, newItem) + newItem = self.search_replace.itemPrototype().clone() + newItem.setText(replace) + self.search_replace.setItem(row,1, newItem) + return row def sr_change_clicked(self): - row = self.opt_search_replace.currentRow() + row = self.search_replace.currentRow() if row >= 0: - self.opt_search_replace.item(row, 0).setText(self.sr_search.regex) - self.opt_search_replace.item(row, 1).setText(self.sr_replace.text()) - self.opt_search_replace.setCurrentCell(row, 0) + self.search_replace.item(row, 0).setText(self.sr_search.regex) + self.search_replace.item(row, 1).setText(self.sr_replace.text()) + self.search_replace.setCurrentCell(row, 0) def sr_remove_clicked(self): - row = self.opt_search_replace.currentRow() + row = self.search_replace.currentRow() if row >= 0: - self.opt_search_replace.removeRow(row) - self.opt_search_replace.setCurrentCell(row-1, 0) + self.search_replace.removeRow(row) + self.search_replace.setCurrentCell(row-1, 0) def sr_load_clicked(self): filename = QFileDialog.getOpenFileName(self, 'Load Calibre Search-Replace definitions file', '.', 'Calibre Search-Replace definitions file (*.csr)') if filename: with open(filename, 'r') as f: val = f.read() - self.set_value(self.opt_search_replace, val) + self.set_value(self.search_replace, val) def sr_save_clicked(self): filename = QFileDialog.getSaveFileName(self, 'Save Calibre Search-Replace definitions file', '.', 'Calibre Search-Replace definitions file (*.csr)') if filename: with open(filename, 'w') as f: - val = self.get_value(self.opt_search_replace) + val = self.get_value(self.search_replace) f.write(val) def sr_currentCellChanged(self, row, column, previousRow, previousColumn) : if row >= 0: self.sr_change.setEnabled(True) self.sr_remove.setEnabled(True) - self.sr_search.set_regex(self.opt_search_replace.item(row, 0).text()) - self.sr_replace.setText(self.opt_search_replace.item(row, 1).text()) + self.sr_search.set_regex(self.search_replace.item(row, 0).text()) + self.sr_replace.setText(self.search_replace.item(row, 1).text()) else: self.sr_change.setEnabled(False) self.sr_remove.setEnabled(False) @@ -114,9 +122,9 @@ class SearchAndReplaceWidget(Widget, Ui_Form): self.sr_search.set_doc(doc) def pre_commit_check(self): - for row in xrange(0, self.opt_search_replace.rowCount()): + for row in xrange(0, self.search_replace.rowCount()): try: - pat = unicode(self.opt_search_replace.item(row,0).text()) + pat = unicode(self.search_replace.item(row,0).text()) re.compile(pat) except Exception as err: error_dialog(self, _('Invalid regular expression'), @@ -124,23 +132,59 @@ class SearchAndReplaceWidget(Widget, Ui_Form): return False return True + # Options + @property + def opt_search_replace(self): + return 'search_replace' + + @property + def opt_sr1_search(self): + return 'sr1_search' + + @property + def opt_sr1_replace(self): + return 'sr1_replace' + + @property + def opt_sr2_search(self): + return 'sr2_search' + + @property + def opt_sr2_replace(self): + return 'sr2_replace' + + @property + def opt_sr3_search(self): + return 'sr3_search' + + @property + def opt_sr3_replace(self): + return 'sr3_replace' + # Options handling def connect_gui_obj_handler(self, g, slot): - if isinstance(g, QTableWidget): - g.cellChanged.connect(slot) + if g == self.opt_search_replace: + self.search_replace.cellChanged.connect(slot) def get_value_handler(self, g): + if g != self.opt_search_replace: + return None + ans = [] - for row in xrange(0, g.rowCount()): + for row in xrange(0, self.search_replace.rowCount()): colItems = [] - for col in xrange(0, g.columnCount()): - colItems.append(unicode(g.item(row, col).text())) + for col in xrange(0, self.search_replace.columnCount()): + colItems.append(unicode(self.search_replace.item(row, col).text())) ans.append(colItems) return json.dumps(ans) def set_value_handler(self, g, val): + if g != self.opt_search_replace: + self.handle_legacy(g, val) + return True + try: rowItems = json.loads(val) if not isinstance(rowItems, list): @@ -148,11 +192,45 @@ class SearchAndReplaceWidget(Widget, Ui_Form): except: rowItems = [] - g.setRowCount(len(rowItems)) + if len(rowItems) == 0: + return True + + self.search_replace.setRowCount(len(rowItems)) for row, colItems in enumerate(rowItems): for col, cellValue in enumerate(colItems): - newItem = g.itemPrototype().clone() + newItem = self.search_replace.itemPrototype().clone() newItem.setText(cellValue) - g.setItem(row,col, newItem) + self.search_replace.setItem(row,col, newItem) return True + + def handle_legacy(self, g, val): + ''' + Handles legacy search/replace options sr1_search, sr1_replace, + sr2_search, sr2_replace, sr3_search, sr3_replace. + Before introducing the search_replace option only three search/replace + definitions could be made. These where stored in the options named above. + This function is for backward compatibility with saved options and for + compatibility with setting sr* options in the CLI. + ''' + + if not val: return + + row = int(g[2]) - 1 # the row to set in the search_replace table is 0 for sr1_*, 1 for sr2_*, etc + col = (0 if g[4] == 's' else 1) # the fourth character in g is 's' for search options and 'r' for replace options + + # add any missing rows + while self.search_replace.rowCount() < row+1: + self.sr_add_row('', '') + + # set the value + self.search_replace.item(row, col).setText(val) + + def setup_help_handler(self, g, help): + if g != self.opt_search_replace: + return True + + self.search_replace._help = help + self.setup_widget_help(self.search_replace) + return True + diff --git a/src/calibre/gui2/convert/search_and_replace.ui b/src/calibre/gui2/convert/search_and_replace.ui index af276d7992..47180ed702 100644 --- a/src/calibre/gui2/convert/search_and_replace.ui +++ b/src/calibre/gui2/convert/search_and_replace.ui @@ -129,7 +129,7 @@ - + 0 From f0708f779e1a18ec09d2589891aaf76950285d57 Mon Sep 17 00:00:00 2001 From: Eli Algranti Date: Fri, 13 Apr 2012 14:04:26 +1000 Subject: [PATCH 8/8] Added CLI support search_replace by changing serialization and deserialization of option. --- src/calibre/ebooks/conversion/plumber.py | 12 +++- src/calibre/ebooks/conversion/preprocess.py | 17 +++++- .../conversion/search_replace_option.py | 50 +++++++++++++++ .../gui2/convert/search_and_replace.py | 61 +++++++++++++------ .../gui2/convert/search_and_replace.ui | 4 +- 5 files changed, 120 insertions(+), 24 deletions(-) create mode 100644 src/calibre/ebooks/conversion/search_replace_option.py diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 88e56c4633..f873b5485d 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -628,7 +628,17 @@ OptionRecommendation(name='sr3_replace', OptionRecommendation(name='search_replace', recommended_value='[]', level=OptionRecommendation.LOW, - help=_('Modify the document text and structure using user defined patterns.')), + help=_('Modify the document text and structure using user defined patterns.' + 'This option accepts parameters in two forms:\n' + '1.file:\n' + 'The file should contain alternating lines or search/replace strings:\n' + ' \n' + ' \n' + ' \n' + ' \n' + 'Files saved through the user interface dialog can be used with this option.\n' + '2.json:= 0: @@ -122,14 +119,40 @@ class SearchAndReplaceWidget(Widget, Ui_Form): self.sr_search.set_doc(doc) def pre_commit_check(self): - for row in xrange(0, self.search_replace.rowCount()): + + + definitions = self.get_definitions() + + # Verify the search/replace in the edit widgets has been + # included to the list of search/replace definitions + + edit_search = self.sr_search.regex + + if edit_search: + edit_replace = unicode(self.sr_replace.text()) + found = False + for search, replace in definitions: + if search == edit_search and replace == edit_replace: + found = True + break + if not found: + msgBox = QMessageBox(self) + msgBox.setText(_('The search / replace definition being edited has not been added to the list of definitions')) + msgBox.setInformativeText(_('Do you wish to continue with the conversion (the definition will not be used)?')) + msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + msgBox.setDefaultButton(QMessageBox.No) + if msgBox.exec_() != QMessageBox.Yes: + return False + + # Verify all search expressions are valid + for search, replace in definitions: try: - pat = unicode(self.search_replace.item(row,0).text()) - re.compile(pat) + re.compile(search) except Exception as err: error_dialog(self, _('Invalid regular expression'), _('Invalid regular expression: %s')%err, show=True) return False + return True # Options @@ -171,14 +194,16 @@ class SearchAndReplaceWidget(Widget, Ui_Form): def get_value_handler(self, g): if g != self.opt_search_replace: return None - + return search_replace_option.encodeJson(self.get_definitions()) + + def get_definitions(self): ans = [] for row in xrange(0, self.search_replace.rowCount()): colItems = [] for col in xrange(0, self.search_replace.columnCount()): colItems.append(unicode(self.search_replace.item(row, col).text())) ans.append(colItems) - return json.dumps(ans) + return ans def set_value_handler(self, g, val): if g != self.opt_search_replace: @@ -186,7 +211,7 @@ class SearchAndReplaceWidget(Widget, Ui_Form): return True try: - rowItems = json.loads(val) + rowItems = search_replace_option.decode(val) if not isinstance(rowItems, list): rowItems = [] except: diff --git a/src/calibre/gui2/convert/search_and_replace.ui b/src/calibre/gui2/convert/search_and_replace.ui index 47180ed702..41063a7eb3 100644 --- a/src/calibre/gui2/convert/search_and_replace.ui +++ b/src/calibre/gui2/convert/search_and_replace.ui @@ -32,7 +32,7 @@ - First expression + Search/Replace Definition Edit @@ -147,7 +147,7 @@ - <p>Search and replace uses <i>regular expressions</i>. See the <a href="http://manual.calibre-ebook.com/regexp.html">regular expressions tutorial</a> to get started with regular expressions. Also clicking the wizard buttons below will allow you to test your regular expression against the current input document. + <p>Search and replace uses <i>regular expressions</i>. See the <a href="http://manual.calibre-ebook.com/regexp.html">regular expressions tutorial</a> to get started with regular expressions. Also clicking the wizard button below will allow you to test your regular expression against the current input document. true