From 7e493d1b01abb73d15211a582f78c50a53962dad Mon Sep 17 00:00:00 2001 From: John Schember Date: Mon, 29 Jun 2009 07:05:47 -0400 Subject: [PATCH 01/31] Search for empty fields using location:none. --- src/calibre/library/database2.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 3550253ffa..1f52f55526 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -208,6 +208,9 @@ class ResultCache(SearchQueryParser): for item in self._data: if item is None: continue for loc in location: + if (not item[loc] or item[loc] == [] or item[loc] == 0 or item[loc] == '') and query == 'none': + matches.add(item[0]) + break if item[loc] and query in item[loc].lower(): matches.add(item[0]) break From 6b8905d272ca7d4231292b41c0098b2337e32827 Mon Sep 17 00:00:00 2001 From: John Schember Date: Mon, 29 Jun 2009 21:28:23 -0400 Subject: [PATCH 02/31] Search by rating:#. Also include rating in default search. --- src/calibre/library/database2.py | 13 +++++++++++-- src/calibre/utils/search_query_parser.py | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 1f52f55526..389cdba8a0 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -198,22 +198,31 @@ class ResultCache(SearchQueryParser): query = query.decode('utf-8') if location in ('tag', 'author', 'format'): location += 's' - all = ('title', 'authors', 'publisher', 'tags', 'comments', 'series', 'formats', 'isbn') + all = ('title', 'authors', 'publisher', 'tags', 'comments', 'series', 'formats', 'isbn', 'rating') MAP = {} for x in all: MAP[x] = FIELD_MAP[x] + EXCLUDE_FIELDS = [MAP['rating']] location = [location] if location != 'all' else list(MAP.keys()) for i, loc in enumerate(location): location[i] = MAP[loc] + try: + rating_query = int(query) * 2 + except: + rating_query = None for item in self._data: if item is None: continue for loc in location: if (not item[loc] or item[loc] == [] or item[loc] == 0 or item[loc] == '') and query == 'none': matches.add(item[0]) break - if item[loc] and query in item[loc].lower(): + if rating_query and item[loc] and loc == MAP['rating'] and rating_query == int(item[loc]): matches.add(item[0]) break + if item[loc] and loc not in EXCLUDE_FIELDS and query in item[loc].lower(): + matches.add(item[0]) + break + return matches def remove(self, id): diff --git a/src/calibre/utils/search_query_parser.py b/src/calibre/utils/search_query_parser.py index 35241c89c4..369afed9b5 100644 --- a/src/calibre/utils/search_query_parser.py +++ b/src/calibre/utils/search_query_parser.py @@ -50,6 +50,7 @@ class SearchQueryParser(object): 'author', 'publisher', 'series', + 'rating', 'comments', 'format', 'isbn', From 840d5670785e714b6370c9c03105fb5b5996cc9a Mon Sep 17 00:00:00 2001 From: John Schember Date: Tue, 30 Jun 2009 21:14:07 -0400 Subject: [PATCH 03/31] Search for empty covers using cover:none --- src/calibre/library/database2.py | 39 +++++++++++++----------- src/calibre/utils/search_query_parser.py | 1 + 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 389cdba8a0..651d9788d7 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -51,7 +51,7 @@ copyfile = os.link if hasattr(os, 'link') else shutil.copyfile FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'publisher':3, 'rating':4, 'timestamp':5, 'size':6, 'tags':7, 'comments':8, 'series':9, 'series_index':10, 'sort':11, 'author_sort':12, 'formats':13, 'isbn':14, 'path':15, - 'lccn':16, 'pubdate':17, 'flags':18} + 'lccn':16, 'pubdate':17, 'flags':18, 'cover':19} INDEX_MAP = dict(zip(FIELD_MAP.values(), FIELD_MAP.keys())) @@ -198,11 +198,11 @@ class ResultCache(SearchQueryParser): query = query.decode('utf-8') if location in ('tag', 'author', 'format'): location += 's' - all = ('title', 'authors', 'publisher', 'tags', 'comments', 'series', 'formats', 'isbn', 'rating') + all = ('title', 'authors', 'publisher', 'tags', 'comments', 'series', 'formats', 'isbn', 'rating', 'cover') MAP = {} for x in all: MAP[x] = FIELD_MAP[x] - EXCLUDE_FIELDS = [MAP['rating']] + EXCLUDE_FIELDS = [MAP['rating'], MAP['cover']] location = [location] if location != 'all' else list(MAP.keys()) for i, loc in enumerate(location): location[i] = MAP[loc] @@ -254,15 +254,16 @@ class ResultCache(SearchQueryParser): pass return False - def refresh_ids(self, conn, ids): + def refresh_ids(self, db, ids): ''' Refresh the data in the cache for books identified by ids. Returns a list of affected rows or None if the rows are filtered. ''' for id in ids: try: - self._data[id] = conn.get('SELECT * from meta WHERE id=?', + self._data[id] = db.conn.get('SELECT * from meta WHERE id=?', (id,))[0] + self._data[id].append(db.cover(id, index_is_id=True, as_path=True)) except IndexError: return None try: @@ -271,12 +272,13 @@ class ResultCache(SearchQueryParser): pass return None - def books_added(self, ids, conn): + def books_added(self, ids, db): if not ids: return self._data.extend(repeat(None, max(ids)-len(self._data)+2)) for id in ids: - self._data[id] = conn.get('SELECT * from meta WHERE id=?', (id,))[0] + self._data[id] = db.conn.get('SELECT * from meta WHERE id=?', (id,))[0] + self._data[id].append(db.cover(id, index_is_id=True, as_path=True)) self._map[0:0] = ids self._map_filtered[0:0] = ids @@ -294,6 +296,9 @@ class ResultCache(SearchQueryParser): self._data = list(itertools.repeat(None, temp[-1][0]+2)) if temp else [] for r in temp: self._data[r[0]] = r + for item in self._data: + if item is not None: + item.append(db.cover(item[0], index_is_id=True, as_path=True)) self._map = [i[0] for i in self._data if i is not None] if field is not None: self.sort(field, ascending) @@ -412,7 +417,7 @@ class LibraryDatabase2(LibraryDatabase): self.refresh = functools.partial(self.data.refresh, self) self.sort = self.data.sort self.index = self.data.index - self.refresh_ids = functools.partial(self.data.refresh_ids, self.conn) + self.refresh_ids = functools.partial(self.data.refresh_ids, self) self.row = self.data.row self.has_id = self.data.has_id self.count = self.data.count @@ -1024,7 +1029,7 @@ class LibraryDatabase2(LibraryDatabase): self.set_rating(id, val, notify=False) elif column == 'tags': self.set_tags(id, val.split(','), append=False, notify=False) - self.data.refresh_ids(self.conn, [id]) + self.data.refresh_ids(self, [id]) self.set_path(id, True) self.notify('metadata', [id]) @@ -1203,7 +1208,7 @@ class LibraryDatabase2(LibraryDatabase): if id: self.conn.execute('DELETE FROM books_tags_link WHERE tag=? AND book=?', (id, book_id)) self.conn.commit() - self.data.refresh_ids(self.conn, [book_id]) + self.data.refresh_ids(self, [book_id]) if notify: self.notify('metadata', [id]) @@ -1308,7 +1313,7 @@ class LibraryDatabase2(LibraryDatabase): obj = self.conn.execute('INSERT INTO books(title, author_sort) VALUES (?, ?)', (mi.title, mi.authors[0])) id = obj.lastrowid - self.data.books_added([id], self.conn) + self.data.books_added([id], self) self.set_path(id, index_is_id=True) self.conn.commit() self.set_metadata(id, mi) @@ -1317,7 +1322,7 @@ class LibraryDatabase2(LibraryDatabase): if not hasattr(path, 'read'): stream.close() self.conn.commit() - self.data.refresh_ids(self.conn, [id]) # Needed to update format list and size + self.data.refresh_ids(self, [id]) # Needed to update format list and size return id def run_import_plugins(self, path_or_stream, format): @@ -1345,7 +1350,7 @@ class LibraryDatabase2(LibraryDatabase): obj = self.conn.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)', (title, series_index, aus)) id = obj.lastrowid - self.data.books_added([id], self.conn) + self.data.books_added([id], self) self.set_path(id, True) self.conn.commit() self.set_metadata(id, mi) @@ -1378,7 +1383,7 @@ class LibraryDatabase2(LibraryDatabase): obj = self.conn.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)', (title, series_index, aus)) id = obj.lastrowid - self.data.books_added([id], self.conn) + self.data.books_added([id], self) ids.append(id) self.set_path(id, True) self.conn.commit() @@ -1389,7 +1394,7 @@ class LibraryDatabase2(LibraryDatabase): self.add_format(id, format, stream, index_is_id=True) stream.close() self.conn.commit() - self.data.refresh_ids(self.conn, ids) # Needed to update format list and size + self.data.refresh_ids(self, ids) # Needed to update format list and size if duplicates: paths = list(duplicate[0] for duplicate in duplicates) formats = list(duplicate[1] for duplicate in duplicates) @@ -1411,7 +1416,7 @@ class LibraryDatabase2(LibraryDatabase): obj = self.conn.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)', (title, series_index, aus)) id = obj.lastrowid - self.data.books_added([id], self.conn) + self.data.books_added([id], self) self.set_path(id, True) self.set_metadata(id, mi) for path in formats: @@ -1420,7 +1425,7 @@ class LibraryDatabase2(LibraryDatabase): continue self.add_format_with_hooks(id, ext, path, index_is_id=True) self.conn.commit() - self.data.refresh_ids(self.conn, [id]) # Needed to update format list and size + self.data.refresh_ids(self, [id]) # Needed to update format list and size if notify: self.notify('add', [id]) diff --git a/src/calibre/utils/search_query_parser.py b/src/calibre/utils/search_query_parser.py index 369afed9b5..425b4c2d49 100644 --- a/src/calibre/utils/search_query_parser.py +++ b/src/calibre/utils/search_query_parser.py @@ -51,6 +51,7 @@ class SearchQueryParser(object): 'publisher', 'series', 'rating', + 'cover', 'comments', 'format', 'isbn', From 4bf1a46acd25346272a53e171176640ad181e0bc Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 1 Jul 2009 06:42:12 -0400 Subject: [PATCH 04/31] Enhance used and empty fields search with :false and :true instead of :none --- src/calibre/library/database2.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 651d9788d7..edf0071a20 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -213,7 +213,10 @@ class ResultCache(SearchQueryParser): for item in self._data: if item is None: continue for loc in location: - if (not item[loc] or item[loc] == [] or item[loc] == 0 or item[loc] == '') and query == 'none': + if query == 'false' and (not item[loc] or item[loc].strip() == ''): + matches.add(item[0]) + break + if query == 'true' and (item[loc] and item[loc].strip() != ''): matches.add(item[0]) break if rating_query and item[loc] and loc == MAP['rating'] and rating_query == int(item[loc]): From 88beebd0a3a389fcc5c51ed8dcddd87c6d017694 Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 1 Jul 2009 06:54:13 -0400 Subject: [PATCH 05/31] Search: use has_cover instead of cover. --- src/calibre/library/database2.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index edf0071a20..43bd6e6434 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -213,10 +213,16 @@ class ResultCache(SearchQueryParser): for item in self._data: if item is None: continue for loc in location: - if query == 'false' and (not item[loc] or item[loc].strip() == ''): + if query == 'false' and not item[loc]: + if isinstance(item[loc], basestring): + if item[loc].strip() != '': + continue matches.add(item[0]) break - if query == 'true' and (item[loc] and item[loc].strip() != ''): + if query == 'true' and item[loc]: + if isinstance(item[loc], basestring): + if item[loc].strip() == '': + continue matches.add(item[0]) break if rating_query and item[loc] and loc == MAP['rating'] and rating_query == int(item[loc]): @@ -266,7 +272,7 @@ class ResultCache(SearchQueryParser): try: self._data[id] = db.conn.get('SELECT * from meta WHERE id=?', (id,))[0] - self._data[id].append(db.cover(id, index_is_id=True, as_path=True)) + self._data[id].append(db.has_cover(id, index_is_id=True)) except IndexError: return None try: @@ -281,7 +287,7 @@ class ResultCache(SearchQueryParser): self._data.extend(repeat(None, max(ids)-len(self._data)+2)) for id in ids: self._data[id] = db.conn.get('SELECT * from meta WHERE id=?', (id,))[0] - self._data[id].append(db.cover(id, index_is_id=True, as_path=True)) + self._data[id].append(db.has_cover(id, index_is_id=True)) self._map[0:0] = ids self._map_filtered[0:0] = ids @@ -301,7 +307,7 @@ class ResultCache(SearchQueryParser): self._data[r[0]] = r for item in self._data: if item is not None: - item.append(db.cover(item[0], index_is_id=True, as_path=True)) + item.append(db.has_cover(item[0], index_is_id=True)) self._map = [i[0] for i in self._data if i is not None] if field is not None: self.sort(field, ascending) From be516ba66b35fa5f37a85f1db8c98b94cd77073d Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 1 Jul 2009 07:42:39 -0400 Subject: [PATCH 06/31] GUI: bulk convert when multiple books selected. --- src/calibre/gui2/main.py | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index e3aa0e6b8c..1fad75fb96 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -312,11 +312,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): cm.addAction(_('Bulk convert')) self.action_convert.setMenu(cm) QObject.connect(cm.actions()[0], - SIGNAL('triggered(bool)'), self.convert_single) + SIGNAL('triggered(bool)'), partial(self.convert_ebook, bulk=False)) QObject.connect(cm.actions()[1], - SIGNAL('triggered(bool)'), self.convert_bulk) + SIGNAL('triggered(bool)'), partial(self.convert_ebook, bulk=True)) QObject.connect(self.action_convert, - SIGNAL('triggered(bool)'), self.convert_single) + SIGNAL('triggered(bool)'), partial(self.convert_ebook, bulk=False)) self.convert_menu = cm pm = QMenu() @@ -1156,32 +1156,17 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): return None return [self.library_view.model().db.id(r) for r in rows] - def convert_bulk(self, checked): + def convert_ebook(self, checked, bulk=None): book_ids = self.get_books_for_conversion() if book_ids is None: return previous = self.library_view.currentIndex() rows = [x.row() for x in \ self.library_view.selectionModel().selectedRows()] - jobs, changed, bad = convert_bulk_ebook(self, + if bulk or (not bulk and len(book_ids) > 1): + jobs, changed, bad = convert_bulk_ebook(self, self.library_view.model().db, book_ids, out_format=prefs['output_format']) - for func, args, desc, fmt, id, temp_files in jobs: - if id not in bad: - job = self.job_manager.run_job(Dispatcher(self.book_converted), - func, args=args, description=desc) - self.conversion_jobs[job] = (temp_files, fmt, id) - - if changed: - self.library_view.model().refresh_rows(rows) - current = self.library_view.currentIndex() - self.library_view.model().current_changed(current, previous) - - def convert_single(self, checked): - book_ids = self.get_books_for_conversion() - if book_ids is None: return - previous = self.library_view.currentIndex() - rows = [x.row() for x in \ - self.library_view.selectionModel().selectedRows()] - jobs, changed, bad = convert_single_ebook(self, + else: + jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, out_format=prefs['output_format']) for func, args, desc, fmt, id, temp_files in jobs: if id not in bad: From 03fe8b6261c6b0fd804b33f57fa5466774d21623 Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 1 Jul 2009 07:52:57 -0400 Subject: [PATCH 07/31] GUI: Convert, allow single convert when specifically selected. --- src/calibre/gui2/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index 1fad75fb96..b1e75a7a9f 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -316,7 +316,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): QObject.connect(cm.actions()[1], SIGNAL('triggered(bool)'), partial(self.convert_ebook, bulk=True)) QObject.connect(self.action_convert, - SIGNAL('triggered(bool)'), partial(self.convert_ebook, bulk=False)) + SIGNAL('triggered(bool)'), partial(self.convert_ebook)) self.convert_menu = cm pm = QMenu() @@ -1162,7 +1162,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): previous = self.library_view.currentIndex() rows = [x.row() for x in \ self.library_view.selectionModel().selectedRows()] - if bulk or (not bulk and len(book_ids) > 1): + if bulk or (bulk is None and len(book_ids) > 1): jobs, changed, bad = convert_bulk_ebook(self, self.library_view.model().db, book_ids, out_format=prefs['output_format']) else: From 8c265f4bea2dd2d3da21601825bf97fdebf8df5f Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 1 Jul 2009 08:17:40 -0400 Subject: [PATCH 08/31] Implement bug #2754: Authors drop down list in edit metadata. --- src/calibre/gui2/dialogs/metadata_single.py | 38 ++++++++++++++------- src/calibre/gui2/dialogs/metadata_single.ui | 14 ++++---- src/calibre/gui2/widgets.py | 2 ++ src/calibre/library/database.py | 4 +++ 4 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index d25d0609c8..2ddbec7f20 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -265,12 +265,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): if not isbn: isbn = '' self.isbn.setText(isbn) - au = self.db.authors(row) - if au: - au = [a.strip().replace('|', ',') for a in au.split(',')] - self.authors.setText(authors_to_string(au)) - else: - self.authors.setText('') aus = self.db.author_sort(row) self.author_sort.setText(aus if aus else '') tags = self.db.tags(row) @@ -295,7 +289,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): Format(self.formats, ext, size) - self.initialize_series_and_publisher() + self.initialize_combos() self.series_index.setValue(self.db.series_index(row)) QObject.connect(self.series, SIGNAL('currentIndexChanged(int)'), self.enable_series_index) @@ -331,6 +325,30 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): def cover_dropped(self): self.cover_changed = True + def initialize_combos(self): + self.initalize_authors() + self.initialize_series() + self.initialize_publisher() + + self.layout().activate() + + def initalize_authors(self): + all_authors = self.db.all_authors() + all_authors.sort(cmp=lambda x, y : cmp(x[1], y[1])) + author_id = self.db.author_id(self.row) + idx, c = None, 0 + for i in all_authors: + id, name = i + if id == author_id: + idx = c + name = [name.strip().replace('|', ',') for n in name.split(',')] + self.authors.addItem(authors_to_string(name)) + c += 1 + + self.authors.setEditText('') + if idx is not None: + self.authors.setCurrentIndex(idx) + def initialize_series(self): self.series.setSizeAdjustPolicy(self.series.AdjustToContentsOnFirstShow) all_series = self.db.all_series() @@ -349,8 +367,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.series.setCurrentIndex(idx) self.enable_series_index() - def initialize_series_and_publisher(self): - self.initialize_series() + def initialize_publisher(self): all_publishers = self.db.all_publishers() all_publishers.sort(cmp=lambda x, y : cmp(x[1], y[1])) publisher_id = self.db.publisher_id(self.row) @@ -366,9 +383,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): if idx is not None: self.publisher.setCurrentIndex(idx) - - self.layout().activate() - def edit_tags(self): d = TagEditor(self, self.db, self.row) d.exec_() diff --git a/src/calibre/gui2/dialogs/metadata_single.ui b/src/calibre/gui2/dialogs/metadata_single.ui index bbf1bb0f7b..ff98d22ad3 100644 --- a/src/calibre/gui2/dialogs/metadata_single.ui +++ b/src/calibre/gui2/dialogs/metadata_single.ui @@ -121,9 +121,6 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - authors - @@ -345,9 +342,6 @@ - - - @@ -371,6 +365,13 @@ + + + + true + + + @@ -655,7 +656,6 @@ title swap_button - authors author_sort auto_author_sort rating diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 3f7734f8c9..abfe137b99 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -493,6 +493,8 @@ class EnComboBox(QComboBox): QComboBox.__init__(self, *args) self.setLineEdit(EnLineEdit(self)) + def text(self): + return qstring_to_unicode(self.currentText()) class PythonHighlighter(QSyntaxHighlighter): diff --git a/src/calibre/library/database.py b/src/calibre/library/database.py index 72b629db0b..ed92853df2 100644 --- a/src/calibre/library/database.py +++ b/src/calibre/library/database.py @@ -928,6 +928,10 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; except: pass + def author_id(self, index, index_is_id=False): + id = index if index_is_id else self.id(index) + return self.conn.get('SELECT author from books_authors_link WHERE book=?', (id,), all=False) + def isbn(self, idx, index_is_id=False): id = idx if index_is_id else self.id(idx) return self.conn.get('SELECT isbn FROM books WHERE id=?',(id,), all=False) From 0427dd55c1e1025527835611f2f77b67cbd54a06 Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 1 Jul 2009 20:43:11 -0400 Subject: [PATCH 09/31] Additional options for case menu in line edits. --- src/calibre/gui2/widgets.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index abfe137b99..159101f04d 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -10,7 +10,8 @@ from PyQt4.Qt import QListView, QIcon, QFont, QLabel, QListWidget, \ QPixmap, QMovie, QPalette, QTimer, QDialog, \ QAbstractListModel, QVariant, Qt, SIGNAL, \ QRegExp, QSettings, QSize, QModelIndex, \ - QAbstractButton, QPainter, QLineEdit, QComboBox + QAbstractButton, QPainter, QLineEdit, QComboBox, \ + QMenu from calibre.gui2 import human_readable, NONE, TableView, \ qstring_to_unicode, error_dialog @@ -460,12 +461,30 @@ class LineEditECM(object): def contextMenuEvent(self, event): menu = self.createStandardContextMenu() menu.addSeparator() - action_title_case = menu.addAction('Title Case') + case_menu = QMenu('Change Case') + action_upper_case = case_menu.addAction('Upper Case') + action_lower_case = case_menu.addAction('Lower Case') + action_swap_case = case_menu.addAction('Swap Case') + action_title_case = case_menu.addAction('Title Case') + + self.connect(action_upper_case, SIGNAL('triggered()'), self.upper_case) + self.connect(action_lower_case, SIGNAL('triggered()'), self.lower_case) + self.connect(action_swap_case, SIGNAL('triggered()'), self.swap_case) self.connect(action_title_case, SIGNAL('triggered()'), self.title_case) + menu.addMenu(case_menu) menu.exec_(event.globalPos()) + def upper_case(self): + self.setText(qstring_to_unicode(self.text()).upper()) + + def lower_case(self): + self.setText(qstring_to_unicode(self.text()).lower()) + + def swap_case(self): + self.setText(qstring_to_unicode(self.text()).swapcase()) + def title_case(self): self.setText(qstring_to_unicode(self.text()).title()) From f72f353ec5f6c6b52d1a86073f74a6f32d5209af Mon Sep 17 00:00:00 2001 From: John Schember Date: Thu, 2 Jul 2009 06:10:27 -0400 Subject: [PATCH 10/31] Make case menu translatable. --- src/calibre/gui2/widgets.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 159101f04d..3bcd1f5d3b 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -462,11 +462,11 @@ class LineEditECM(object): menu = self.createStandardContextMenu() menu.addSeparator() - case_menu = QMenu('Change Case') - action_upper_case = case_menu.addAction('Upper Case') - action_lower_case = case_menu.addAction('Lower Case') - action_swap_case = case_menu.addAction('Swap Case') - action_title_case = case_menu.addAction('Title Case') + case_menu = QMenu(_('Change Case')) + action_upper_case = case_menu.addAction(_('Upper Case')) + action_lower_case = case_menu.addAction(_('Lower Case')) + action_swap_case = case_menu.addAction(_('Swap Case')) + action_title_case = case_menu.addAction(_('Title Case')) self.connect(action_upper_case, SIGNAL('triggered()'), self.upper_case) self.connect(action_lower_case, SIGNAL('triggered()'), self.lower_case) From 4ad6939e1709bddbcd0433208d8e9053eb175155 Mon Sep 17 00:00:00 2001 From: John Schember Date: Thu, 2 Jul 2009 18:43:07 -0400 Subject: [PATCH 11/31] Use EnLineEdit in library and device view. --- src/calibre/gui2/library.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py index c3134de917..878f7ddc2f 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library.py @@ -18,6 +18,7 @@ from calibre.ptempfile import PersistentTemporaryFile from calibre.library.database2 import FIELD_MAP from calibre.gui2 import NONE, TableView, qstring_to_unicode, config, \ error_dialog +from calibre.gui2.widgets import EnLineEdit from calibre.utils.search_query_parser import SearchQueryParser from calibre.ebooks.metadata.meta import set_metadata as _set_metadata from calibre.ebooks.metadata import string_to_authors, fmt_sidx @@ -110,6 +111,11 @@ class PubDateDelegate(QStyledItemDelegate): qde.setCalendarPopup(True) return qde +class TextDelegate(QStyledItemDelegate): + + def createEditor(self, parent, option, index): + editor = EnLineEdit(parent) + return editor class BooksModel(QAbstractTableModel): headers = { @@ -659,6 +665,8 @@ class BooksView(TableView): self.setModel(self._model) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSortingEnabled(True) + for i in range(10): + self.setItemDelegateForColumn(i, TextDelegate(self)) try: cm = self._model.column_map self.columns_sorted(cm.index('rating') if 'rating' in cm else -1, @@ -768,7 +776,7 @@ class DeviceBooksView(BooksView): self.resize_on_select = False self.rating_delegate = None for i in range(10): - self.setItemDelegateForColumn(i, self.itemDelegate()) + self.setItemDelegateForColumn(i, TextDelegate(self)) self.setDragDropMode(self.NoDragDrop) self.setAcceptDrops(False) From 4594402dfc945b364da39c56be85778d7230ad03 Mon Sep 17 00:00:00 2001 From: John Schember Date: Thu, 2 Jul 2009 21:03:56 -0400 Subject: [PATCH 12/31] Fix bug #2764: Don't append ttt to end of tags for Kindle metadata page. --- src/calibre/customize/profiles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py index 4d8b8a0113..f641984124 100644 --- a/src/calibre/customize/profiles.py +++ b/src/calibre/customize/profiles.py @@ -233,7 +233,7 @@ class KindleOutput(OutputProfile): @classmethod def tags_to_string(cls, tags): - return 'ttt '.join(tags)+'ttt ' + return ', '.join(tags) class KindleDXOutput(OutputProfile): @@ -248,7 +248,7 @@ class KindleDXOutput(OutputProfile): @classmethod def tags_to_string(cls, tags): - return 'ttt '.join(tags)+'ttt ' + return ', '.join(tags) output_profiles = [OutputProfile, SonyReaderOutput, MSReaderOutput, From 8e1f51d8cb907bff4da1215cc505e3524e30795c Mon Sep 17 00:00:00 2001 From: John Schember Date: Fri, 3 Jul 2009 08:02:52 -0400 Subject: [PATCH 13/31] Revert tag change. The ttt is used for searching. --- src/calibre/customize/profiles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py index f641984124..4d8b8a0113 100644 --- a/src/calibre/customize/profiles.py +++ b/src/calibre/customize/profiles.py @@ -233,7 +233,7 @@ class KindleOutput(OutputProfile): @classmethod def tags_to_string(cls, tags): - return ', '.join(tags) + return 'ttt '.join(tags)+'ttt ' class KindleDXOutput(OutputProfile): @@ -248,7 +248,7 @@ class KindleDXOutput(OutputProfile): @classmethod def tags_to_string(cls, tags): - return ', '.join(tags) + return 'ttt '.join(tags)+'ttt ' output_profiles = [OutputProfile, SonyReaderOutput, MSReaderOutput, From eb896d010f4e1661b35664fa51f04e94ac3fa5f3 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sat, 4 Jul 2009 14:14:53 -0400 Subject: [PATCH 14/31] PDF Input: User can specify regex to use to remove header and footer. Preprocessor: Able to use options from input plugins. --- src/calibre/ebooks/conversion/plumber.py | 2 +- src/calibre/ebooks/conversion/preprocess.py | 33 +++++++++++++-------- src/calibre/ebooks/pdf/input.py | 19 +++++++----- src/calibre/gui2/convert/pdf_input.py | 32 +++++++++++++++++++- src/calibre/gui2/convert/pdf_input.ui | 30 +++++++++++++++---- 5 files changed, 90 insertions(+), 26 deletions(-) diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 11975094e3..77ae507867 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -694,7 +694,7 @@ def create_oebbook(log, path_or_stream, opts, input_plugin, reader=None, ''' from calibre.ebooks.oeb.base import OEBBook html_preprocessor = HTMLPreProcessor(input_plugin.preprocess_html, - opts.preprocess_html, getattr(opts, 'pdf_line_length', 0.5)) + opts.preprocess_html, opts) oeb = OEBBook(log, html_preprocessor, pretty_print=opts.pretty_print, input_encoding=encoding) if not populate: diff --git a/src/calibre/ebooks/conversion/preprocess.py b/src/calibre/ebooks/conversion/preprocess.py index 43bb52b8ad..f9788fdba8 100644 --- a/src/calibre/ebooks/conversion/preprocess.py +++ b/src/calibre/ebooks/conversion/preprocess.py @@ -140,8 +140,6 @@ class HTMLPreProcessor(object): (re.compile(u'(?<=[\.,;\?!”"\'])[\s^ ]*(?=<)'), lambda match: ' '), # Connect paragraphs split by - (re.compile(u'(?<=[^\s][-–])[\s]*(

)*[\s]*(

)*\s*(?=[^\s])'), lambda match: ''), - # Remove - that splits words - (re.compile(u'(?<=[^\s])[-–]+(?=[^\s])'), lambda match: ''), # Add space before and after italics (re.compile(u'(?'), lambda match: ' '), (re.compile(r'(?=\w)'), lambda match: ' '), @@ -163,10 +161,10 @@ class HTMLPreProcessor(object): lambda match : '

%s

'%(match.group(1),)), ] def __init__(self, input_plugin_preprocess, plugin_preprocess, - pdf_line_length): + extra_opts=None): self.input_plugin_preprocess = input_plugin_preprocess self.plugin_preprocess = plugin_preprocess - self.pdf_line_length = pdf_line_length + self.extra_opts = extra_opts def is_baen(self, src): return re.compile(r')?\s*()\s*(?=(<(i|b|u)>)?\s*[\w\d(])' % length, re.UNICODE), wrap_lines), - ] + start_rules = [] + end_rules = [] - rules = self.PDFTOHTML + line_length_rules + if getattr(self.extra_opts, 'remove_header', None): + start_rules.append( + (re.compile(getattr(self.extra_opts, 'header_regex')), lambda match : '') + ) + if getattr(self.extra_opts, 'remove_footer', None): + start_rules.append( + (re.compile(getattr(self.extra_opts, 'footer_regex')), lambda match : '') + ) + if getattr(self.extra_opts, 'unwrap_factor', None): + length = line_length(html, getattr(self.extra_opts, 'unwrap_factor')) + if length: + end_rules.append( + # Un wrap using punctuation + (re.compile(r'(?<=.{%i}[a-z\.,;:)-IA])\s*(?P)?\s*()\s*(?=(<(i|b|u)>)?\s*[\w\d(])' % length, re.UNICODE), wrap_lines), + ) + + rules = start_rules + self.PDFTOHTML + end_rules else: rules = [] for rule in self.PREPROCESS + rules: diff --git a/src/calibre/ebooks/pdf/input.py b/src/calibre/ebooks/pdf/input.py index 3b82becc1f..e17d50869e 100644 --- a/src/calibre/ebooks/pdf/input.py +++ b/src/calibre/ebooks/pdf/input.py @@ -20,10 +20,20 @@ class PDFInput(InputFormatPlugin): options = set([ OptionRecommendation(name='no_images', recommended_value=False, help=_('Do not extract images from the document')), - OptionRecommendation(name='pdf_line_length', recommended_value=0.5, + OptionRecommendation(name='unwrap_factor', recommended_value=0.5, help=_('Scale used to determine the length at which a line should ' 'be unwrapped. Valid values are a decimal between 0 and 1. The ' 'default is 0.5, this is the median line length.')), + OptionRecommendation(name='remove_header', recommended_value=False, + help=_('Use a regular expression to try and remove the header.')), + OptionRecommendation(name='header_regex', + recommended_value='(?i)(?<=
)((\s*(()*
\s*)?\d+
\s*.*?\s*)|(\s*(()*
\s*)?.*?
\s*\d+))(?=
)', + help=_('The regular expression to use to remove the header.')), + OptionRecommendation(name='remove_footer', recommended_value=False, + help=_('Use a regular expression to try and remove the footer.')), + OptionRecommendation(name='footer_regex', + recommended_value='(?i)(?<=
)((\s*(()*
\s*)?\d+
\s*.*?\s*)|(\s*(()*
\s*)?.*?
\s*\d+))(?=
)', + help=_('The regular expression to use to remove the footer.')), ]) def convert(self, stream, options, file_ext, log, @@ -42,12 +52,7 @@ class PDFInput(InputFormatPlugin): images = os.listdir(os.getcwd()) images.remove('index.html') for i in images: - # Remove the - from the file name because it causes problems. - # The reference to the image with the - will be changed to not - # include it later in the conversion process. - new_i = i.replace('-', '') - os.rename(i, new_i) - manifest.append((new_i, None)) + manifest.append((i, None)) log.debug('Generating manifest...') opf.create_manifest(manifest) diff --git a/src/calibre/gui2/convert/pdf_input.py b/src/calibre/gui2/convert/pdf_input.py index 71e4bc0ef3..bfd658526c 100644 --- a/src/calibre/gui2/convert/pdf_input.py +++ b/src/calibre/gui2/convert/pdf_input.py @@ -4,8 +4,13 @@ __license__ = 'GPL 3' __copyright__ = '2009, John Schember ' __docformat__ = 'restructuredtext en' +import re + +from PyQt4.Qt import SIGNAL + from calibre.gui2.convert.pdf_input_ui import Ui_Form from calibre.gui2.convert import Widget +from calibre.gui2 import qstring_to_unicode, error_dialog class PluginWidget(Widget, Ui_Form): @@ -14,6 +19,31 @@ class PluginWidget(Widget, Ui_Form): def __init__(self, parent, get_option, get_help, db=None, book_id=None): Widget.__init__(self, parent, 'pdf_input', - ['no_images', 'pdf_line_length']) + ['no_images', 'unwrap_factor', 'remove_header', 'header_regex', + 'remove_footer', 'footer_regex']) self.db, self.book_id = db, book_id self.initialize_options(get_option, get_help, db, book_id) + + self.opt_header_regex.setEnabled(self.opt_remove_header.isChecked()) + self.opt_footer_regex.setEnabled(self.opt_remove_footer.isChecked()) + + self.connect(self.opt_remove_header, SIGNAL('stateChanged(int)'), self.header_regex_state) + self.connect(self.opt_remove_footer, SIGNAL('stateChanged(int)'), self.footer_regex_state) + + def header_regex_state(self, state): + self.opt_header_regex.setEnabled(state) + + def footer_regex_state(self, state): + self.opt_footer_regex.setEnabled(state) + + def pre_commit_check(self): + for x in ('header_regex', 'footer_regex'): + x = getattr(self, 'opt_'+x) + try: + pat = qstring_to_unicode(x.text()) + re.compile(pat) + except Exception, err: + error_dialog(self, _('Invalid regular expression'), + _('Invalid regular expression: %s')%err).exec_() + return False + return True diff --git a/src/calibre/gui2/convert/pdf_input.ui b/src/calibre/gui2/convert/pdf_input.ui index 35b840ded0..d34c6d404b 100644 --- a/src/calibre/gui2/convert/pdf_input.ui +++ b/src/calibre/gui2/convert/pdf_input.ui @@ -14,14 +14,14 @@ Form - + Line Un-Wrapping Factor: - + Qt::Vertical @@ -34,8 +34,8 @@ - - + + 1.000000000000000 @@ -47,13 +47,33 @@ - + No Images + + + + Remove Header + + + + + + + Remove Footer + + + + + + + + + From 5dbc4252a77b7a0f42e79d91f6a7e6193dd8c012 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sat, 4 Jul 2009 14:51:05 -0400 Subject: [PATCH 15/31] Plumber: Dummy mode that loads all options from all formats so that the default values will be set in preferences. --- src/calibre/ebooks/conversion/plumber.py | 31 ++++++++++++++++++------ src/calibre/gui2/dialogs/config.py | 2 +- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 77ae507867..e33df27412 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -7,7 +7,8 @@ import os, re, sys from calibre.customize.conversion import OptionRecommendation, DummyReporter from calibre.customize.ui import input_profiles, output_profiles, \ - plugin_for_input_format, plugin_for_output_format + plugin_for_input_format, plugin_for_output_format, \ + available_input_formats, available_output_formats from calibre.ebooks.conversion.preprocess import HTMLPreProcessor from calibre.ptempfile import PersistentTemporaryDirectory from calibre import extract, walk @@ -50,7 +51,7 @@ class Plumber(object): 'tags', 'book_producer', 'language' ] - def __init__(self, input, output, log, report_progress=DummyReporter()): + def __init__(self, input, output, log, report_progress=DummyReporter(), dummy=False): ''' :param input: Path to input file. :param output: Path to output file/directory @@ -419,12 +420,28 @@ OptionRecommendation(name='list_recipes', self.input_fmt = input_fmt self.output_fmt = output_fmt + + self.all_format_options = set() + self.input_options = set() + self.output_options = set() # Build set of all possible options. Two options are equal if their # names are the same. - self.input_options = self.input_plugin.options.union( - self.input_plugin.common_options) - self.output_options = self.output_plugin.options.union( + if not dummy: + self.input_options = self.input_plugin.options.union( + self.input_plugin.common_options) + self.output_options = self.output_plugin.options.union( self.output_plugin.common_options) + else: + for fmt in available_input_formats(): + input_plugin = plugin_for_input_format(fmt) + if input_plugin: + self.all_format_options = self.all_format_options.union( + input_plugin.options.union(input_plugin.common_options)) + for fmt in available_output_formats(): + output_plugin = plugin_for_output_format(fmt) + if output_plugin: + self.all_format_options = self.all_format_options.union( + output_plugin.options.union(output_plugin.common_options)) # Remove the options that have been disabled by recommendations from the # plugins. @@ -469,7 +486,7 @@ OptionRecommendation(name='list_recipes', def get_option_by_name(self, name): for group in (self.input_options, self.pipeline_options, - self.output_options): + self.output_options, self.all_format_options): for rec in group: if rec.option == name: return rec @@ -535,7 +552,7 @@ OptionRecommendation(name='list_recipes', ''' self.opts = OptionValues() for group in (self.input_options, self.pipeline_options, - self.output_options): + self.output_options, self.all_format_options): for rec in group: setattr(self.opts, rec.option.name, rec.recommended_value) diff --git a/src/calibre/gui2/dialogs/config.py b/src/calibre/gui2/dialogs/config.py index 2b5e9d4cf2..aacccbd6b9 100644 --- a/src/calibre/gui2/dialogs/config.py +++ b/src/calibre/gui2/dialogs/config.py @@ -39,7 +39,7 @@ class ConfigTabs(QTabWidget): log = Log() log.outputs = [] - self.plumber = Plumber('dummt.epub', 'dummy.epub', log) + self.plumber = Plumber('dummy.epub', 'dummy.epub', log, dummy=True) def widget_factory(cls): return cls(self, self.plumber.get_option_by_name, From 00bfb182ea93367fab1b59f3c82998e4476d7c67 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sat, 4 Jul 2009 20:51:22 -0400 Subject: [PATCH 16/31] Implement bug #2775: Auto complete tags in booksview. --- src/calibre/gui2/library.py | 89 ++++++++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py index 878f7ddc2f..086d62962c 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library.py @@ -9,7 +9,8 @@ from math import cos, sin, pi from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \ QItemDelegate, QPainterPath, QLinearGradient, QBrush, \ QPen, QStyle, QPainter, QLineEdit, \ - QPalette, QImage, QApplication, QMenu, QStyledItemDelegate + QPalette, QImage, QApplication, QMenu, \ + QStyledItemDelegate, QCompleter, QStringListModel from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \ SIGNAL, QObject, QSize, QModelIndex, QDate @@ -117,6 +118,74 @@ class TextDelegate(QStyledItemDelegate): editor = EnLineEdit(parent) return editor +class CompleterLineEdit(EnLineEdit): + + def __init__(self, *args): + EnLineEdit.__init__(self, *args) + + QObject.connect(self, SIGNAL('textChanged(QString)'), self.text_changed) + + def text_changed(self, text): + all_text = qstring_to_unicode(text) + text = all_text[:self.cursorPosition()] + prefix = text.split(',')[-1].strip() + + text_tags = [] + for t in all_text.split(','): + t1 = qstring_to_unicode(t).strip() + if t1 != '': + text_tags.append(t) + text_tags = list(set(text_tags)) + + self.emit(SIGNAL('text_changed(PyQt_PyObject, PyQt_PyObject)'), text_tags, prefix) + + def complete_text(self, text): + cursor_pos = self.cursorPosition() + before_text = qstring_to_unicode(self.text())[:cursor_pos] + after_text = qstring_to_unicode(self.text())[cursor_pos:] + prefix_len = len(before_text.split(',')[-1].strip()) + self.setText('%s%s, %s' % (before_text[:cursor_pos - prefix_len], text, after_text)) + self.setCursorPosition(cursor_pos - prefix_len + len(text) + 2) + +class TagsCompleter(QCompleter): + + def __init__(self, parent, all_tags): + QCompleter.__init__(self, all_tags, parent) + self.all_tags = set(all_tags) + + def update(self, text_tags, completion_prefix): + tags = list(self.all_tags.difference(text_tags)) + model = QStringListModel(tags, self) + self.setModel(model) + + self.setCompletionPrefix(completion_prefix) + if completion_prefix.strip() != '': + self.complete() + +class TagsDelegate(QStyledItemDelegate): + + def __init__(self, parent): + QStyledItemDelegate.__init__(self, parent) + self.db = None + + def set_database(self, db): + self.db = db + + def createEditor(self, parent, option, index): + editor = CompleterLineEdit(parent) + if self.db: + completer = TagsCompleter(editor, self.db.all_tags()) + completer.setCaseSensitivity(Qt.CaseInsensitive) + + QObject.connect(editor, + SIGNAL('text_changed(PyQt_PyObject, PyQt_PyObject)'), + completer.update) + QObject.connect(completer, SIGNAL('activated(QString)'), + editor.complete_text) + + completer.setWidget(editor) + return editor + class BooksModel(QAbstractTableModel): headers = { 'title' : _("Title"), @@ -165,8 +234,13 @@ class BooksModel(QAbstractTableModel): pidx = self.column_map.index('pubdate') except ValueError: pidx = -1 + try: + taidx = self.column_map.index('tags') + except ValueError: + taidx = -1 - self.emit(SIGNAL('columns_sorted(int,int,int)'), idx, tidx, pidx) + self.emit(SIGNAL('columns_sorted(int,int,int,int)'), idx, tidx, pidx, + taidx) def set_database(self, db): @@ -660,6 +734,7 @@ class BooksView(TableView): self.rating_delegate = LibraryDelegate(self) self.timestamp_delegate = DateDelegate(self) self.pubdate_delegate = PubDateDelegate(self) + self.tags_delegate = TagsDelegate(self) self.display_parent = parent self._model = modelcls(self) self.setModel(self._model) @@ -671,15 +746,16 @@ class BooksView(TableView): cm = self._model.column_map self.columns_sorted(cm.index('rating') if 'rating' in cm else -1, cm.index('timestamp') if 'timestamp' in cm else -1, - cm.index('pubdate') if 'pubdate' in cm else -1) + cm.index('pubdate') if 'pubdate' in cm else -1, + cm.index('tags') if 'tags' in cm else -1) except ValueError: pass QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'), self._model.current_changed) - self.connect(self._model, SIGNAL('columns_sorted(int,int,int)'), + self.connect(self._model, SIGNAL('columns_sorted(int,int,int,int)'), self.columns_sorted, Qt.QueuedConnection) - def columns_sorted(self, rating_col, timestamp_col, pubdate_col): + def columns_sorted(self, rating_col, timestamp_col, pubdate_col, tags_col): for i in range(self.model().columnCount(None)): if self.itemDelegateForColumn(i) in (self.rating_delegate, self.timestamp_delegate, self.pubdate_delegate): @@ -690,6 +766,8 @@ class BooksView(TableView): self.setItemDelegateForColumn(timestamp_col, self.timestamp_delegate) if pubdate_col > -1: self.setItemDelegateForColumn(pubdate_col, self.pubdate_delegate) + if tags_col > -1: + self.setItemDelegateForColumn(tags_col, self.tags_delegate) def set_context_menu(self, edit_metadata, send_to_device, convert, view, save, open_folder, book_details, similar_menu=None): @@ -752,6 +830,7 @@ class BooksView(TableView): def set_database(self, db): self._model.set_database(db) + self.tags_delegate.set_database(db) def close(self): self._model.close() From 74dc8d5b1d6e26ad117db2c30a9614d51db6769f Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 5 Jul 2009 10:08:08 -0400 Subject: [PATCH 17/31] Preferred input format: User orderable list of input formats to prefer. --- src/calibre/customize/ui.py | 7 ++++ src/calibre/gui2/__init__.py | 7 +++- src/calibre/gui2/dialogs/config.py | 28 +++++++++++++- src/calibre/gui2/dialogs/config.ui | 62 ++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 3 deletions(-) diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py index 78afa3be15..04f9b80529 100644 --- a/src/calibre/customize/ui.py +++ b/src/calibre/customize/ui.py @@ -276,6 +276,13 @@ def plugin_for_input_format(fmt): if fmt.lower() in plugin.file_types: return plugin +def all_input_formats(): + formats = set([]) + for plugin in input_format_plugins(): + for format in plugin.file_types: + formats.add(format) + return formats + def available_input_formats(): formats = set([]) for plugin in input_format_plugins(): diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index af4ca16eac..a7e088ee1c 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -11,6 +11,7 @@ ORG_NAME = 'KovidsBrain' APP_UID = 'libprs500' from calibre import islinux, iswindows from calibre.startup import get_lang +from calibre.customize.ui import all_input_formats from calibre.utils.config import Config, ConfigProxy, dynamic import calibre.resources as resources from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats @@ -48,9 +49,11 @@ def _config(): help=_('Defaults for conversion to LRF')) c.add_opt('LRF_ebook_viewer_options', default=None, help=_('Options for the LRF ebook viewer')) - c.add_opt('internally_viewed_formats', default=['LRF', 'EPUB', 'LIT', - 'MOBI', 'PRC', 'HTML', 'FB2', 'PDB', 'RB'], + c.add_opt('internally_viewed_formats', default=all_input_formats(), help=_('Formats that are viewed using the internal viewer')) + c.add_opt('input_format_order', default=['EPUB', 'MOBI', 'PRC', 'LIT', + 'HTML', 'FB2', 'PDB', 'RB'], + help=_('Order list of formats to prefer for input.')) c.add_opt('column_map', default=ALL_COLUMNS, help=_('Columns to be displayed in the book list')) c.add_opt('autolaunch_server', default=False, help=_('Automatically launch content server on application startup')) diff --git a/src/calibre/gui2/dialogs/config.py b/src/calibre/gui2/dialogs/config.py index aacccbd6b9..20ade05846 100644 --- a/src/calibre/gui2/dialogs/config.py +++ b/src/calibre/gui2/dialogs/config.py @@ -22,7 +22,8 @@ from calibre.library import server_config from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \ disable_plugin, customize_plugin, \ plugin_customization, add_plugin, \ - remove_plugin, input_format_plugins, \ + remove_plugin, all_input_formats, \ + input_format_plugins, \ output_format_plugins, available_output_formats from calibre.utils.smtp import config as smtp_prefs from calibre.gui2.convert.look_and_feel import LookAndFeelWidget @@ -337,6 +338,18 @@ class ConfigDialog(QDialog, Ui_Dialog): self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse) self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact) + input_map = config['input_format_order'] + all_formats = set() + for fmt in all_input_formats(): + all_formats.add(fmt.upper()) + for format in input_map + list(all_formats.difference(input_map)): + item = QListWidgetItem(format, self.input_order) + item.setData(Qt.UserRole, QVariant(format)) + item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsSelectable) + + self.connect(self.input_up, SIGNAL('clicked()'), self.up_input) + self.connect(self.input_down, SIGNAL('clicked()'), self.down_input) + dirs = config['frequently_used_directories'] rn = config['use_roman_numerals_for_series_number'] self.timeout.setValue(prefs['network_timeout']) @@ -553,6 +566,17 @@ class ConfigDialog(QDialog, Ui_Dialog): plugin.name + _(' cannot be removed. It is a ' 'builtin plugin. Try disabling it instead.')).exec_() + def up_input(self): + idx = self.input_order.currentRow() + if idx > 0: + self.input_order.insertItem(idx-1, self.input_order.takeItem(idx)) + self.input_order.setCurrentRow(idx-1) + + def down_input(self): + idx = self.input_order.currentRow() + if idx < self.input_order.count()-1: + self.input_order.insertItem(idx+1, self.input_order.takeItem(idx)) + self.input_order.setCurrentRow(idx+1) def up_column(self): idx = self.columns.currentRow() @@ -656,6 +680,8 @@ class ConfigDialog(QDialog, Ui_Dialog): config['new_version_notification'] = bool(self.new_version_notification.isChecked()) prefs['network_timeout'] = int(self.timeout.value()) path = qstring_to_unicode(self.location.text()) + input_cols = [unicode(self.input_order.item(i).data(Qt.UserRole).toString()) for i in range(self.input_order.count())] + config['input_format_order'] = input_cols cols = [unicode(self.columns.item(i).data(Qt.UserRole).toString()) for i in range(self.columns.count()) if self.columns.item(i).checkState()==Qt.Checked] if not cols: cols = ['title'] diff --git a/src/calibre/gui2/dialogs/config.ui b/src/calibre/gui2/dialogs/config.ui index 90a53364ca..f2cf9b3202 100644 --- a/src/calibre/gui2/dialogs/config.ui +++ b/src/calibre/gui2/dialogs/config.ui @@ -232,6 +232,68 @@ + + + + Preferred &input format order: + + + + + + + + true + + + QAbstractItemView::SelectRows + + + + + + + + + ... + + + + :/images/arrow-up.svg:/images/arrow-up.svg + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + ... + + + + :/images/arrow-down.svg:/images/arrow-down.svg + + + + + + + + + + From a3361e36f69855dbe4f549d4e0e8b3dc59f0fc12 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 5 Jul 2009 10:38:57 -0400 Subject: [PATCH 18/31] Use preferred output format and input format order when converting via gui. --- src/calibre/ebooks/conversion/plumber.py | 4 ---- src/calibre/gui2/__init__.py | 4 ++-- src/calibre/gui2/convert/bulk.py | 6 +++--- src/calibre/gui2/convert/single.py | 15 +++++++-------- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index e33df27412..fa807eb24f 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -20,10 +20,6 @@ def supported_input_formats(): fmts.add(x) return fmts -INPUT_FORMAT_PREFERENCES = ['cbr', 'cbz', 'cbc', 'lit', 'mobi', 'prc', 'azw', 'fb2', 'html', - 'rtf', 'pdf', 'txt', 'pdb'] -OUTPUT_FORMAT_PREFERENCES = ['epub', 'mobi', 'lit', 'pdf', 'pdb', 'txt'] - class OptionValues(object): pass diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index a7e088ee1c..934d0b8f2f 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -11,7 +11,6 @@ ORG_NAME = 'KovidsBrain' APP_UID = 'libprs500' from calibre import islinux, iswindows from calibre.startup import get_lang -from calibre.customize.ui import all_input_formats from calibre.utils.config import Config, ConfigProxy, dynamic import calibre.resources as resources from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats @@ -49,7 +48,8 @@ def _config(): help=_('Defaults for conversion to LRF')) c.add_opt('LRF_ebook_viewer_options', default=None, help=_('Options for the LRF ebook viewer')) - c.add_opt('internally_viewed_formats', default=all_input_formats(), + c.add_opt('internally_viewed_formats', default=['LRF', 'EPUB', 'LIT', + 'MOBI', 'PRC', 'HTML', 'FB2', 'PDB', 'RB'], help=_('Formats that are viewed using the internal viewer')) c.add_opt('input_format_order', default=['EPUB', 'MOBI', 'PRC', 'LIT', 'HTML', 'FB2', 'PDB', 'RB'], diff --git a/src/calibre/gui2/convert/bulk.py b/src/calibre/gui2/convert/bulk.py index 0b48f2521b..393e005e5c 100644 --- a/src/calibre/gui2/convert/bulk.py +++ b/src/calibre/gui2/convert/bulk.py @@ -15,7 +15,8 @@ from calibre.gui2.convert.page_setup import PageSetupWidget from calibre.gui2.convert.structure_detection import StructureDetectionWidget from calibre.gui2.convert.toc import TOCWidget from calibre.gui2.convert import GuiRecommendations -from calibre.ebooks.conversion.plumber import Plumber, OUTPUT_FORMAT_PREFERENCES +from calibre.ebooks.conversion.plumber import Plumber +from calibre.utils.config import prefs from calibre.utils.logging import Log class BulkConfig(Config): @@ -102,7 +103,7 @@ class BulkConfig(Config): preferred_output_format = preferred_output_format if \ preferred_output_format and preferred_output_format \ in output_formats else sort_formats_by_preference(output_formats, - OUTPUT_FORMAT_PREFERENCES)[0] + prefs['output_format'])[0] self.output_formats.addItems(list(map(QString, [x.upper() for x in output_formats]))) self.output_formats.setCurrentIndex(output_formats.index(preferred_output_format)) @@ -117,4 +118,3 @@ class BulkConfig(Config): self._recommendations = recs ResizableDialog.accept(self) - diff --git a/src/calibre/gui2/convert/single.py b/src/calibre/gui2/convert/single.py index ea839e6e80..6d04c7ab04 100644 --- a/src/calibre/gui2/convert/single.py +++ b/src/calibre/gui2/convert/single.py @@ -10,7 +10,7 @@ import sys, cPickle from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont -from calibre.gui2 import ResizableDialog, NONE +from calibre.gui2 import ResizableDialog, NONE, config from calibre.ebooks.conversion.config import GuiRecommendations, save_specifics, \ load_specifics from calibre.gui2.convert.single_ui import Ui_Dialog @@ -20,11 +20,10 @@ from calibre.gui2.convert.page_setup import PageSetupWidget from calibre.gui2.convert.structure_detection import StructureDetectionWidget from calibre.gui2.convert.toc import TOCWidget - -from calibre.ebooks.conversion.plumber import Plumber, supported_input_formats, \ - INPUT_FORMAT_PREFERENCES, OUTPUT_FORMAT_PREFERENCES +from calibre.ebooks.conversion.plumber import Plumber, supported_input_formats from calibre.customize.ui import available_output_formats from calibre.customize.conversion import OptionRecommendation +from calibre.utils.config import prefs from calibre.utils.logging import Log class NoSupportedInputFormats(Exception): @@ -33,11 +32,11 @@ class NoSupportedInputFormats(Exception): def sort_formats_by_preference(formats, prefs): def fcmp(x, y): try: - x = prefs.index(x) + x = prefs.index(x.upper()) except ValueError: x = sys.maxint try: - y = prefs.index(y) + y = prefs.index(y.upper()) except ValueError: y = sys.maxint return cmp(x, y) @@ -206,11 +205,11 @@ class Config(ResizableDialog, Ui_Dialog): preferred_input_format = preferred_input_format if \ preferred_input_format in input_formats else \ sort_formats_by_preference(input_formats, - INPUT_FORMAT_PREFERENCES)[0] + config['input_format_order'])[0] preferred_output_format = preferred_output_format if \ preferred_output_format in output_formats else \ sort_formats_by_preference(output_formats, - OUTPUT_FORMAT_PREFERENCES)[0] + prefs['output_format'])[0] self.input_formats.addItems(list(map(QString, [x.upper() for x in input_formats]))) self.output_formats.addItems(list(map(QString, [x.upper() for x in From 83abb08b90b4141ecc1699d7b8c2ffa7bf157e31 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 5 Jul 2009 10:48:21 -0400 Subject: [PATCH 19/31] use prefers for input format order instead of config. --- src/calibre/gui2/__init__.py | 3 --- src/calibre/gui2/convert/single.py | 4 ++-- src/calibre/gui2/dialogs/config.py | 4 ++-- src/calibre/utils/config.py | 2 ++ 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 934d0b8f2f..af4ca16eac 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -51,9 +51,6 @@ def _config(): c.add_opt('internally_viewed_formats', default=['LRF', 'EPUB', 'LIT', 'MOBI', 'PRC', 'HTML', 'FB2', 'PDB', 'RB'], help=_('Formats that are viewed using the internal viewer')) - c.add_opt('input_format_order', default=['EPUB', 'MOBI', 'PRC', 'LIT', - 'HTML', 'FB2', 'PDB', 'RB'], - help=_('Order list of formats to prefer for input.')) c.add_opt('column_map', default=ALL_COLUMNS, help=_('Columns to be displayed in the book list')) c.add_opt('autolaunch_server', default=False, help=_('Automatically launch content server on application startup')) diff --git a/src/calibre/gui2/convert/single.py b/src/calibre/gui2/convert/single.py index 6d04c7ab04..b995c3e3a2 100644 --- a/src/calibre/gui2/convert/single.py +++ b/src/calibre/gui2/convert/single.py @@ -10,7 +10,7 @@ import sys, cPickle from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont -from calibre.gui2 import ResizableDialog, NONE, config +from calibre.gui2 import ResizableDialog, NONE from calibre.ebooks.conversion.config import GuiRecommendations, save_specifics, \ load_specifics from calibre.gui2.convert.single_ui import Ui_Dialog @@ -205,7 +205,7 @@ class Config(ResizableDialog, Ui_Dialog): preferred_input_format = preferred_input_format if \ preferred_input_format in input_formats else \ sort_formats_by_preference(input_formats, - config['input_format_order'])[0] + prefs['input_format_order'])[0] preferred_output_format = preferred_output_format if \ preferred_output_format in output_formats else \ sort_formats_by_preference(output_formats, diff --git a/src/calibre/gui2/dialogs/config.py b/src/calibre/gui2/dialogs/config.py index 20ade05846..1c337788c6 100644 --- a/src/calibre/gui2/dialogs/config.py +++ b/src/calibre/gui2/dialogs/config.py @@ -338,7 +338,7 @@ class ConfigDialog(QDialog, Ui_Dialog): self.connect(self.browse_button, SIGNAL('clicked(bool)'), self.browse) self.connect(self.compact_button, SIGNAL('clicked(bool)'), self.compact) - input_map = config['input_format_order'] + input_map = prefs['input_format_order'] all_formats = set() for fmt in all_input_formats(): all_formats.add(fmt.upper()) @@ -681,7 +681,7 @@ class ConfigDialog(QDialog, Ui_Dialog): prefs['network_timeout'] = int(self.timeout.value()) path = qstring_to_unicode(self.location.text()) input_cols = [unicode(self.input_order.item(i).data(Qt.UserRole).toString()) for i in range(self.input_order.count())] - config['input_format_order'] = input_cols + prefs['input_format_order'] = input_cols cols = [unicode(self.columns.item(i).data(Qt.UserRole).toString()) for i in range(self.columns.count()) if self.columns.item(i).checkState()==Qt.Checked] if not cols: cols = ['title'] diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py index 0925d8667d..e225406dff 100644 --- a/src/calibre/utils/config.py +++ b/src/calibre/utils/config.py @@ -548,6 +548,8 @@ def _prefs(): help=_('The language in which to display the user interface')) c.add_opt('output_format', default='EPUB', help=_('The default output format for ebook conversions.')) + c.add_opt('input_format_order', default=['EPUB', 'MOBI', 'PRC', 'LIT'], + help=_('Order list of formats to prefer for input.')) c.add_opt('read_file_metadata', default=True, help=_('Read metadata from files')) c.add_opt('worker_process_priority', default='normal', From 78b3b9ba29ae4c01bdba71730d2ca97e752bf008 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 5 Jul 2009 11:47:25 -0400 Subject: [PATCH 20/31] GUI: View Book respects input plugins and can be used to view multiple books. Also, respects input format preferrence ordering. --- src/calibre/gui2/main.py | 68 +++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index b1e75a7a9f..968bc016fd 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -47,6 +47,7 @@ from calibre.gui2.dialogs.book_info import BookInfo from calibre.ebooks import BOOK_EXTENSIONS from calibre.library.database2 import LibraryDatabase2, CoverCache from calibre.gui2.dialogs.confirm_delete import confirm +from calibre.customize.ui import available_input_formats ADDRESS = r'\\.\pipe\CalibreGUI' if iswindows else \ os.path.expanduser('~/.calibre-gui.socket') @@ -1349,51 +1350,48 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): def view_book(self, triggered): rows = self.current_view().selectionModel().selectedRows() - if self.current_view() is self.library_view: - if not rows or len(rows) == 0: - self._launch_viewer() - return - row = rows[0].row() - formats = self.library_view.model().db.formats(row).upper() - formats = formats.split(',') - title = self.library_view.model().db.title(row) - id = self.library_view.model().db.id(row) - format = None - if len(formats) == 1: - format = formats[0] - if 'LRF' in formats: - format = 'LRF' - if 'EPUB' in formats: - format = 'EPUB' - if 'MOBI' in formats: - format = 'MOBI' - if not formats: - d = error_dialog(self, _('Cannot view'), - _('%s has no available formats.')%(title,)) - d.exec_() - return - if format is None: - d = ChooseFormatDialog(self, _('Choose the format to view'), - formats) - d.exec_() - if d.result() == QDialog.Accepted: - format = d.format() - else: + if not rows or len(rows) == 0: + self._launch_viewer() + return + + if len(rows) >= 3: + if not question_dialog(self, _('Multiple Books Selected'), + _('You are attempting to open %i books. Opening to many ' + 'books at once can be slow and have an negative effect on the ' + 'responsiveness of your computer. Once started the process ' + 'cannot be stopped until complete. Do you wish to continue?' + % len(rows))): return + + if self.current_view() is self.library_view: + for row in rows: + row = row.row() - self.view_format(row, format) + formats = self.library_view.model().db.formats(row).lower() + formats = set(formats.split(',')).intersection(available_input_formats()) + title = self.library_view.model().db.title(row) + + if not formats: + error_dialog(self, _('Cannot view'), + _('%s has no available formats.')%(title,), show=True) + continue + + print prefs['input_format_order'] + for format in prefs['input_format_order']: + if format.lower() in formats: + self.view_format(row, format) + break else: paths = self.current_view().model().paths(rows) - if paths: + for path in paths: pt = PersistentTemporaryFile('_viewer_'+\ - os.path.splitext(paths[0])[1]) + os.path.splitext(path)[1]) self.persistent_files.append(pt) pt.close() self.device_manager.view_book(\ Dispatcher(self.book_downloaded_for_viewing), - paths[0], pt.name) - + path, pt.name) ############################################################################ From 768f2e5cbf029c960b1aab3fbcf73bab9edab939 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 5 Jul 2009 11:54:46 -0400 Subject: [PATCH 21/31] Remove testing print. --- src/calibre/gui2/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index 968bc016fd..927e73b519 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -1377,7 +1377,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): _('%s has no available formats.')%(title,), show=True) continue - print prefs['input_format_order'] for format in prefs['input_format_order']: if format.lower() in formats: self.view_format(row, format) From 60bc41b33ef0a25237ccd28e5d37143b6c562e2a Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 5 Jul 2009 12:43:26 -0400 Subject: [PATCH 22/31] GUI: view ebook, open formats that do not have an input plugin because those can be opened with an external viewer. --- src/calibre/gui2/main.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index 927e73b519..cd1e545a29 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -1368,8 +1368,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): for row in rows: row = row.row() - formats = self.library_view.model().db.formats(row).lower() - formats = set(formats.split(',')).intersection(available_input_formats()) + formats = self.library_view.model().db.formats(row).upper() + formats = formats.split(',') title = self.library_view.model().db.title(row) if not formats: @@ -1377,10 +1377,14 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): _('%s has no available formats.')%(title,), show=True) continue + in_prefs = False for format in prefs['input_format_order']: - if format.lower() in formats: + if format in formats: + in_prefs = True self.view_format(row, format) break + if not in_prefs: + self.view_format(row, format[0]) else: paths = self.current_view().model().paths(rows) for path in paths: From b8fc9de71d09392a07119f6ce03f2f6cc08d2232 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 5 Jul 2009 15:39:35 -0400 Subject: [PATCH 23/31] Make tags completing line edit a stand alone widget. Use tag completing line edit in metadata single dialog. --- src/calibre/gui2/dialogs/metadata_single.py | 4 +- src/calibre/gui2/dialogs/metadata_single.ui | 7 +- src/calibre/gui2/library.py | 60 ++-------------- src/calibre/gui2/widgets.py | 80 ++++++++++++++++++++- 4 files changed, 92 insertions(+), 59 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 2ddbec7f20..68cce6ec5f 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -268,7 +268,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): aus = self.db.author_sort(row) self.author_sort.setText(aus if aus else '') tags = self.db.tags(row) - self.tags.setText(tags if tags else '') + self.tags.setText(', '.join(tags.split(',')) if tags else '') + self.tags.update_tags_cache(self.db.all_tags()) rating = self.db.rating(row) if rating > 0: self.rating.setValue(int(rating/2.)) @@ -389,6 +390,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): if d.result() == QDialog.Accepted: tag_string = ', '.join(d.tags) self.tags.setText(tag_string) + self.tags.update_tags_cache(self.db.all_tags()) def fetch_cover(self): isbn = unicode(self.isbn.text()).strip() diff --git a/src/calibre/gui2/dialogs/metadata_single.ui b/src/calibre/gui2/dialogs/metadata_single.ui index ff98d22ad3..14191f2851 100644 --- a/src/calibre/gui2/dialogs/metadata_single.ui +++ b/src/calibre/gui2/dialogs/metadata_single.ui @@ -222,7 +222,7 @@ - + Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas. @@ -652,6 +652,11 @@ QComboBox
widgets.h
+ + TagsLineEdit + QLineEdit +
widgets.h
+
title diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py index 086d62962c..b7dd91271b 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library.py @@ -19,7 +19,7 @@ from calibre.ptempfile import PersistentTemporaryFile from calibre.library.database2 import FIELD_MAP from calibre.gui2 import NONE, TableView, qstring_to_unicode, config, \ error_dialog -from calibre.gui2.widgets import EnLineEdit +from calibre.gui2.widgets import EnLineEdit, TagsLineEdit from calibre.utils.search_query_parser import SearchQueryParser from calibre.ebooks.metadata.meta import set_metadata as _set_metadata from calibre.ebooks.metadata import string_to_authors, fmt_sidx @@ -118,50 +118,6 @@ class TextDelegate(QStyledItemDelegate): editor = EnLineEdit(parent) return editor -class CompleterLineEdit(EnLineEdit): - - def __init__(self, *args): - EnLineEdit.__init__(self, *args) - - QObject.connect(self, SIGNAL('textChanged(QString)'), self.text_changed) - - def text_changed(self, text): - all_text = qstring_to_unicode(text) - text = all_text[:self.cursorPosition()] - prefix = text.split(',')[-1].strip() - - text_tags = [] - for t in all_text.split(','): - t1 = qstring_to_unicode(t).strip() - if t1 != '': - text_tags.append(t) - text_tags = list(set(text_tags)) - - self.emit(SIGNAL('text_changed(PyQt_PyObject, PyQt_PyObject)'), text_tags, prefix) - - def complete_text(self, text): - cursor_pos = self.cursorPosition() - before_text = qstring_to_unicode(self.text())[:cursor_pos] - after_text = qstring_to_unicode(self.text())[cursor_pos:] - prefix_len = len(before_text.split(',')[-1].strip()) - self.setText('%s%s, %s' % (before_text[:cursor_pos - prefix_len], text, after_text)) - self.setCursorPosition(cursor_pos - prefix_len + len(text) + 2) - -class TagsCompleter(QCompleter): - - def __init__(self, parent, all_tags): - QCompleter.__init__(self, all_tags, parent) - self.all_tags = set(all_tags) - - def update(self, text_tags, completion_prefix): - tags = list(self.all_tags.difference(text_tags)) - model = QStringListModel(tags, self) - self.setModel(model) - - self.setCompletionPrefix(completion_prefix) - if completion_prefix.strip() != '': - self.complete() - class TagsDelegate(QStyledItemDelegate): def __init__(self, parent): @@ -172,18 +128,10 @@ class TagsDelegate(QStyledItemDelegate): self.db = db def createEditor(self, parent, option, index): - editor = CompleterLineEdit(parent) if self.db: - completer = TagsCompleter(editor, self.db.all_tags()) - completer.setCaseSensitivity(Qt.CaseInsensitive) - - QObject.connect(editor, - SIGNAL('text_changed(PyQt_PyObject, PyQt_PyObject)'), - completer.update) - QObject.connect(completer, SIGNAL('activated(QString)'), - editor.complete_text) - - completer.setWidget(editor) + editor = TagsLineEdit(parent, self.db.all_tags()) + else: + editor = EnLineEdit(parent) return editor class BooksModel(QAbstractTableModel): diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 3bcd1f5d3b..f4b9130fc8 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -11,7 +11,7 @@ from PyQt4.Qt import QListView, QIcon, QFont, QLabel, QListWidget, \ QAbstractListModel, QVariant, Qt, SIGNAL, \ QRegExp, QSettings, QSize, QModelIndex, \ QAbstractButton, QPainter, QLineEdit, QComboBox, \ - QMenu + QMenu, QStringListModel, QCompleter from calibre.gui2 import human_readable, NONE, TableView, \ qstring_to_unicode, error_dialog @@ -500,6 +500,84 @@ class EnLineEdit(LineEditECM, QLineEdit): pass +class TagsCompleter(QCompleter): + + ''' + A completer object that completes a list of tags. It is used in conjunction + with a CompleterLineEdit. + ''' + + def __init__(self, parent, all_tags): + QCompleter.__init__(self, all_tags, parent) + self.all_tags = set(all_tags) + + def update(self, text_tags, completion_prefix): + tags = list(self.all_tags.difference(text_tags)) + model = QStringListModel(tags, self) + self.setModel(model) + + self.setCompletionPrefix(completion_prefix) + if completion_prefix.strip() != '': + self.complete() + + def update_tags_cache(self, tags): + self.all_tags = set(tags) + model = QStringListModel(tags, self) + self.setModel(model) + + +class TagsLineEdit(EnLineEdit): + + ''' + A QLineEdit that can complete parts of text separated by separator. + ''' + + def __init__(self, parent=0, tags=[]): + EnLineEdit.__init__(self, parent) + + self.separator = ',' + + self.connect(self, SIGNAL('textChanged(QString)'), self.text_changed) + + self.completer = TagsCompleter(self, tags) + self.completer.setCaseSensitivity(Qt.CaseInsensitive) + + self.connect(self, + SIGNAL('text_changed(PyQt_PyObject, PyQt_PyObject)'), + self.completer.update) + self.connect(self.completer, SIGNAL('activated(QString)'), + self.complete_text) + + self.completer.setWidget(self) + + def update_tags_cache(self, tags): + self.completer.update_tags_cache(tags) + + def text_changed(self, text): + all_text = qstring_to_unicode(text) + text = all_text[:self.cursorPosition()] + prefix = text.split(',')[-1].strip() + + text_tags = [] + for t in all_text.split(self.separator): + t1 = qstring_to_unicode(t).strip() + if t1 != '': + text_tags.append(t) + text_tags = list(set(text_tags)) + + self.emit(SIGNAL('text_changed(PyQt_PyObject, PyQt_PyObject)'), + text_tags, prefix) + + def complete_text(self, text): + cursor_pos = self.cursorPosition() + before_text = qstring_to_unicode(self.text())[:cursor_pos] + after_text = qstring_to_unicode(self.text())[cursor_pos:] + prefix_len = len(before_text.split(',')[-1].strip()) + self.setText('%s%s%s %s' % (before_text[:cursor_pos - prefix_len], + text, self.separator, after_text)) + self.setCursorPosition(cursor_pos - prefix_len + len(text) + 2) + + class EnComboBox(QComboBox): ''' From cf8137ec853788edbd2c3ca58660b44b6d0905d5 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 5 Jul 2009 15:49:53 -0400 Subject: [PATCH 24/31] Metadata single, remove author completer because authors is now a combobox. --- src/calibre/gui2/dialogs/metadata_single.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 68cce6ec5f..7894bb05bb 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -80,13 +80,6 @@ class Format(QListWidgetItem): QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext), text, parent, QListWidgetItem.UserType) -class AuthorCompleter(QCompleter): - - def __init__(self, db): - all_authors = db.all_authors() - all_authors.sort(cmp=lambda x, y : cmp(x[1], y[1])) - QCompleter.__init__(self, [x[1] for x in all_authors]) - class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): COVER_FETCH_TIMEOUT = 240 # seconds @@ -233,8 +226,6 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.cover_changed = False self.cpixmap = None self.cover.setAcceptDrops(True) - self._author_completer = AuthorCompleter(self.db) - self.authors.setCompleter(self._author_completer) self.pubdate.setMinimumDate(QDate(100,1,1)) self.connect(self.cover, SIGNAL('cover_changed()'), self.cover_dropped) QObject.connect(self.cover_button, SIGNAL("clicked(bool)"), \ From b70c1d1231c6c81b7dcbaa2f6f5fb22ce51926fa Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 5 Jul 2009 16:03:03 -0400 Subject: [PATCH 25/31] Metadata Bulk: Auto completing for various inputs. --- src/calibre/gui2/dialogs/metadata_bulk.py | 48 ++++++++++++++++++++--- src/calibre/gui2/dialogs/metadata_bulk.ui | 46 ++++++++++------------ 2 files changed, 63 insertions(+), 31 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 9b8810b3a4..5b3b7fa3d4 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -9,7 +9,8 @@ from PyQt4.QtGui import QDialog from calibre.gui2 import qstring_to_unicode from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog from calibre.gui2.dialogs.tag_editor import TagEditor -from calibre.ebooks.metadata import string_to_authors, authors_to_sort_string +from calibre.ebooks.metadata import string_to_authors, authors_to_sort_string, \ + authors_to_string class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): @@ -25,29 +26,64 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): QObject.connect(self.button_box, SIGNAL("accepted()"), self.sync) QObject.connect(self.rating, SIGNAL('valueChanged(int)'), self.rating_changed) - all_series = self.db.all_series() + self.tags.update_tags_cache(self.db.all_tags()) + self.remove_tags.update_tags_cache(self.db.all_tags()) - for i in all_series: - id, name = i - self.series.addItem(name) + self.initialize_combos() for f in self.db.all_formats(): self.remove_format.addItem(f) self.remove_format.setCurrentIndex(-1) - self.series.lineEdit().setText('') QObject.connect(self.series, SIGNAL('currentIndexChanged(int)'), self.series_changed) QObject.connect(self.series, SIGNAL('editTextChanged(QString)'), self.series_changed) QObject.connect(self.tag_editor_button, SIGNAL('clicked()'), self.tag_editor) + self.exec_() + + def initialize_combos(self): + self.initalize_authors() + self.initialize_series() + self.initialize_publisher() + + def initalize_authors(self): + all_authors = self.db.all_authors() + all_authors.sort(cmp=lambda x, y : cmp(x[1], y[1])) + + for i in all_authors: + id, name = i + name = authors_to_string([name.strip().replace('|', ',') for n in name.split(',')]) + self.authors.addItem(name) + self.authors.setEditText('') + + def initialize_series(self): + all_series = self.db.all_series() + all_series.sort(cmp=lambda x, y : cmp(x[1], y[1])) + + for i in all_series: + id, name = i + self.series.addItem(name) + self.series.setEditText('') + + def initialize_publisher(self): + all_publishers = self.db.all_publishers() + all_publishers.sort(cmp=lambda x, y : cmp(x[1], y[1])) + + for i in all_publishers: + id, name = i + self.publisher.addItem(name) + self.publisher.setEditText('') + def tag_editor(self): d = TagEditor(self, self.db, None) d.exec_() if d.result() == QDialog.Accepted: tag_string = ', '.join(d.tags) self.tags.setText(tag_string) + self.tags.update_tags_cache(self.db.all_tags()) + self.remove_tags.update_tags_cache(self.db.all_tags()) def sync(self): for id in self.ids: diff --git a/src/calibre/gui2/dialogs/metadata_bulk.ui b/src/calibre/gui2/dialogs/metadata_bulk.ui index beea2cace3..f2bcc3cd93 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.ui +++ b/src/calibre/gui2/dialogs/metadata_bulk.ui @@ -45,16 +45,6 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - authors - -
-
- - - - Change the author(s) of this book. Multiple authors should be separated by an &. If the author name contains an &, use && to represent it. - @@ -65,9 +55,6 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - authors -
@@ -117,16 +104,6 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - publisher - - - - - - - Change the publisher of this book - @@ -143,7 +120,7 @@ - + Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas. @@ -174,7 +151,7 @@ - + Comma separated list of tags to remove from the books. @@ -235,6 +212,20 @@ + + + + true + + + + + + + true + + + @@ -265,6 +256,11 @@ QComboBox
widgets.h
+ + TagsLineEdit + QLineEdit +
widgets.h
+
From 4b338c527419a9e6ccd5780f3d9e815b46cb611f Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 5 Jul 2009 18:35:23 -0400 Subject: [PATCH 26/31] Auto completes in convert dialog. --- src/calibre/gui2/convert/metadata.py | 47 ++++++++++++++++++----- src/calibre/gui2/convert/metadata.ui | 43 ++++++++++----------- src/calibre/gui2/dialogs/metadata_bulk.py | 1 - 3 files changed, 59 insertions(+), 32 deletions(-) diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py index 01eb5bee1c..82e7b21148 100644 --- a/src/calibre/gui2/convert/metadata.py +++ b/src/calibre/gui2/convert/metadata.py @@ -35,21 +35,17 @@ class MetadataWidget(Widget, Ui_Form): self.connect(self.cover_button, SIGNAL("clicked()"), self.select_cover) def initialize_metadata_options(self): - all_series = self.db.all_series() - all_series.sort(cmp=lambda x, y : cmp(x[1], y[1])) - for series in all_series: - self.series.addItem(series[1]) - self.series.setCurrentIndex(-1) + self.initialize_combos() mi = self.db.get_metadata(self.book_id, index_is_id=True) self.title.setText(mi.title) if mi.authors: - self.author.setText(authors_to_string(mi.authors)) - else: - self.author.setText('') - self.publisher.setText(mi.publisher if mi.publisher else '') + self.author.setCurrentIndex(self.author.findText(authors_to_string(mi.authors))) + if mi.publisher: + self.publisher.setCurrentIndex(self.publisher.findText(mi.publisher)) self.author_sort.setText(mi.author_sort if mi.author_sort else '') self.tags.setText(', '.join(mi.tags if mi.tags else [])) + self.tags.update_tags_cache(self.db.all_tags()) self.comment.setText(mi.comments if mi.comments else '') if mi.series: self.series.setCurrentIndex(self.series.findText(mi.series)) @@ -66,6 +62,39 @@ class MetadataWidget(Widget, Ui_Form): if not pm.isNull(): self.cover.setPixmap(pm) + def initialize_combos(self): + self.initalize_authors() + self.initialize_series() + self.initialize_publisher() + + def initalize_authors(self): + all_authors = self.db.all_authors() + all_authors.sort(cmp=lambda x, y : cmp(x[1], y[1])) + + for i in all_authors: + id, name = i + name = authors_to_string([name.strip().replace('|', ',') for n in name.split(',')]) + self.author.addItem(name) + self.author.setCurrentIndex(-1) + + def initialize_series(self): + all_series = self.db.all_series() + all_series.sort(cmp=lambda x, y : cmp(x[1], y[1])) + + for i in all_series: + id, name = i + self.series.addItem(name) + self.series.setCurrentIndex(-1) + + def initialize_publisher(self): + all_publishers = self.db.all_publishers() + all_publishers.sort(cmp=lambda x, y : cmp(x[1], y[1])) + + for i in all_publishers: + id, name = i + self.publisher.addItem(name) + self.publisher.setCurrentIndex(-1) + def get_title_and_authors(self): title = unicode(self.title.text()).strip() if not title: diff --git a/src/calibre/gui2/convert/metadata.ui b/src/calibre/gui2/convert/metadata.ui index e4533de24c..3abe8ece55 100644 --- a/src/calibre/gui2/convert/metadata.ui +++ b/src/calibre/gui2/convert/metadata.ui @@ -143,19 +143,6 @@ - - - - - 1 - 0 - - - - Change the author(s) of this book. Multiple authors should be separated by an &. If the author name contains an &, use && to represent it. - - - @@ -195,13 +182,6 @@ - - - - Change the publisher of this book - - - @@ -216,7 +196,7 @@ - + Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas. @@ -276,6 +256,20 @@ + + + + true + + + + + + + true + + + @@ -329,11 +323,16 @@ QComboBox
widgets.h
+ + TagsLineEdit + QLineEdit +
widgets.h
+
- + diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index 5b3b7fa3d4..622ea95a2b 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -42,7 +42,6 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): self.exec_() - def initialize_combos(self): self.initalize_authors() self.initialize_series() From a2457cb9396b52f1048a8aa7ba99b607f7fec959 Mon Sep 17 00:00:00 2001 From: John Schember Date: Sun, 5 Jul 2009 21:16:21 -0400 Subject: [PATCH 27/31] Auto complete author, series, publisher in library_view --- src/calibre/gui2/library.py | 85 ++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 39 deletions(-) diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py index a795573cb6..2197c79b50 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library.py @@ -10,7 +10,7 @@ from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \ QItemDelegate, QPainterPath, QLinearGradient, QBrush, \ QPen, QStyle, QPainter, QLineEdit, \ QPalette, QImage, QApplication, QMenu, \ - QStyledItemDelegate, QCompleter, QStringListModel + QStyledItemDelegate, QCompleter from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, QString, \ SIGNAL, QObject, QSize, QModelIndex, QDate @@ -114,8 +114,26 @@ class PubDateDelegate(QStyledItemDelegate): class TextDelegate(QStyledItemDelegate): + def __init__(self, parent): + ''' + Delegate for text data. If auto_complete_function needs to return a list + of text items to auto-complete with. The funciton is None no + auto-complete will be used. + ''' + QStyledItemDelegate.__init__(self, parent) + self.auto_complete_function = None + + def set_auto_complete_function(self, f): + self.auto_complete_function = f + def createEditor(self, parent, option, index): editor = EnLineEdit(parent) + if self.auto_complete_function: + complete_items = [i[1] for i in self.auto_complete_function()] + completer = QCompleter(complete_items, self) + completer.setCaseSensitivity(Qt.CaseInsensitive) + completer.setCompletionMode(QCompleter.InlineCompletion) + editor.setCompleter(completer) return editor class TagsDelegate(QStyledItemDelegate): @@ -170,26 +188,7 @@ class BooksModel(QAbstractTableModel): if cols != self.column_map: self.column_map = cols self.reset() - try: - idx = self.column_map.index('rating') - except ValueError: - idx = -1 - try: - tidx = self.column_map.index('timestamp') - except ValueError: - tidx = -1 - try: - pidx = self.column_map.index('pubdate') - except ValueError: - pidx = -1 - try: - taidx = self.column_map.index('tags') - except ValueError: - taidx = -1 - - self.emit(SIGNAL('columns_sorted(int,int,int,int)'), idx, tidx, pidx, - taidx) - + self.emit(SIGNAL('columns_sorted()')) def set_database(self, db): self.db = db @@ -670,6 +669,9 @@ class BooksView(TableView): self.timestamp_delegate = DateDelegate(self) self.pubdate_delegate = PubDateDelegate(self) self.tags_delegate = TagsDelegate(self) + self.authors_delegate = TextDelegate(self) + self.series_delegate = TextDelegate(self) + self.publisher_delegate = TextDelegate(self) self.display_parent = parent self._model = modelcls(self) self.setModel(self._model) @@ -677,32 +679,34 @@ class BooksView(TableView): self.setSortingEnabled(True) for i in range(10): self.setItemDelegateForColumn(i, TextDelegate(self)) - try: - cm = self._model.column_map - self.columns_sorted(cm.index('rating') if 'rating' in cm else -1, - cm.index('timestamp') if 'timestamp' in cm else -1, - cm.index('pubdate') if 'pubdate' in cm else -1, - cm.index('tags') if 'tags' in cm else -1) - except ValueError: - pass + self.columns_sorted() QObject.connect(self.selectionModel(), SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'), self._model.current_changed) - self.connect(self._model, SIGNAL('columns_sorted(int,int,int,int)'), + self.connect(self._model, SIGNAL('columns_sorted()'), self.columns_sorted, Qt.QueuedConnection) - def columns_sorted(self, rating_col, timestamp_col, pubdate_col, tags_col): + def columns_sorted(self): for i in range(self.model().columnCount(None)): if self.itemDelegateForColumn(i) in (self.rating_delegate, self.timestamp_delegate, self.pubdate_delegate): self.setItemDelegateForColumn(i, self.itemDelegate()) - if rating_col > -1: - self.setItemDelegateForColumn(rating_col, self.rating_delegate) - if timestamp_col > -1: - self.setItemDelegateForColumn(timestamp_col, self.timestamp_delegate) - if pubdate_col > -1: - self.setItemDelegateForColumn(pubdate_col, self.pubdate_delegate) - if tags_col > -1: - self.setItemDelegateForColumn(tags_col, self.tags_delegate) + + cm = self._model.column_map + + if 'rating' in cm: + self.setItemDelegateForColumn(cm.index('rating'), self.rating_delegate) + if 'timestamp' in cm: + self.setItemDelegateForColumn(cm.index('timestamp'), self.timestamp_delegate) + if 'pubdate' in cm: + self.setItemDelegateForColumn(cm.index('pubdate'), self.pubdate_delegate) + if 'tags' in cm: + self.setItemDelegateForColumn(cm.index('tags'), self.tags_delegate) + if 'authors' in cm: + self.setItemDelegateForColumn(cm.index('authors'), self.authors_delegate) + if 'publisher' in cm: + self.setItemDelegateForColumn(cm.index('publisher'), self.publisher_delegate) + if 'series' in cm: + self.setItemDelegateForColumn(cm.index('series'), self.series_delegate) def set_context_menu(self, edit_metadata, send_to_device, convert, view, save, open_folder, book_details, similar_menu=None): @@ -766,6 +770,9 @@ class BooksView(TableView): def set_database(self, db): self._model.set_database(db) self.tags_delegate.set_database(db) + self.authors_delegate.set_auto_complete_function(db.all_authors) + self.series_delegate.set_auto_complete_function(db.all_series) + self.publisher_delegate.set_auto_complete_function(db.all_publishers) def close(self): self._model.close() From 768979168ccacf8d815fb110fc1f617d4caae28b Mon Sep 17 00:00:00 2001 From: John Schember Date: Tue, 7 Jul 2009 21:14:38 -0400 Subject: [PATCH 28/31] DeviceBooksView does not need auto complete. --- src/calibre/gui2/library.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py index 627de41c74..3d5a30ec05 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library.py @@ -806,6 +806,9 @@ class DeviceBooksView(BooksView): self.setDragDropMode(self.NoDragDrop) self.setAcceptDrops(False) + def set_database(self, db): + self._model.set_database(db) + def resizeColumnsToContents(self): QTableView.resizeColumnsToContents(self) self.columns_resized = True From f86229c72dd4a7e2d5a1ea389220876aa5f972ff Mon Sep 17 00:00:00 2001 From: John Schember Date: Tue, 7 Jul 2009 21:19:03 -0400 Subject: [PATCH 29/31] Cybook Opus profile. --- src/calibre/customize/profiles.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py index 45026fcb5c..9114dc9eee 100644 --- a/src/calibre/customize/profiles.py +++ b/src/calibre/customize/profiles.py @@ -110,6 +110,18 @@ class CybookG3Input(InputProfile): fbase = 16 fsizes = [12, 12, 14, 16, 18, 20, 22, 24] +class CybookOpusInput(InputProfile): + + name = 'Cybook Opus' + short_name = 'cybook_opus' + description = _('This profile is intended for the Cybook Opus.') + + # Screen size is a best guess + screen_size = (600, 800) + dpi = 200 + fbase = 16 + fsizes = [12, 12, 14, 16, 18, 20, 22, 24] + class KindleInput(InputProfile): name = 'Kindle' @@ -219,6 +231,18 @@ class CybookG3Output(OutputProfile): fbase = 16 fsizes = [12, 12, 14, 16, 18, 20, 22, 24] +class CybookOpusOutput(OutputProfile): + + name = 'Cybook Opus' + short_name = 'cybook_opus' + description = _('This profile is intended for the Cybook Opus.') + + # Screen size is a best guess + screen_size = (600, 800) + dpi = 200 + fbase = 16 + fsizes = [12, 12, 14, 16, 18, 20, 22, 24] + class KindleOutput(OutputProfile): name = 'Kindle' From 21140bc72b319c62faaef1ba3762516401679c5f Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 8 Jul 2009 07:31:37 -0400 Subject: [PATCH 30/31] Implement bug #810: Search can be as you type or only on Enter/Return. --- src/calibre/gui2/__init__.py | 3 +++ src/calibre/gui2/dialogs/config.py | 2 ++ src/calibre/gui2/dialogs/config.ui | 21 +++++++++++---------- src/calibre/gui2/library.py | 25 ++++++++++++++++++------- src/calibre/gui2/main.py | 2 ++ 5 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index af4ca16eac..dc1ca7f2bd 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -71,6 +71,9 @@ def _config(): help='Show donation button') c.add_opt('asked_library_thing_password', default=False, help='Asked library thing password at least once.') + c.add_opt('search_as_you_type', default=True, + help='Start searching as you type. If this is disabled then seaerch will ' + 'only take place when the Enter or Return key is pressed.') return ConfigProxy(c) config = _config() diff --git a/src/calibre/gui2/dialogs/config.py b/src/calibre/gui2/dialogs/config.py index 1c337788c6..52b2caeaea 100644 --- a/src/calibre/gui2/dialogs/config.py +++ b/src/calibre/gui2/dialogs/config.py @@ -437,6 +437,7 @@ class ConfigDialog(QDialog, Ui_Dialog): self.password.setText(opts.password if opts.password else '') self.auto_launch.setChecked(config['autolaunch_server']) self.systray_icon.setChecked(config['systray_icon']) + self.search_as_you_type.setChecked(config['search_as_you_type']) self.sync_news.setChecked(config['upload_news_to_device']) self.delete_news.setChecked(config['delete_news_from_library_on_upload']) p = {'normal':0, 'high':1, 'low':2}[prefs['worker_process_priority']] @@ -707,6 +708,7 @@ class ConfigDialog(QDialog, Ui_Dialog): sc.set('max_cover', mcs) config['delete_news_from_library_on_upload'] = self.delete_news.isChecked() config['upload_news_to_device'] = self.sync_news.isChecked() + config['search_as_you_type'] = self.search_as_you_type.isChecked() fmts = [] for i in range(self.viewer.count()): if self.viewer.item(i).checkState() == Qt.Checked: diff --git a/src/calibre/gui2/dialogs/config.ui b/src/calibre/gui2/dialogs/config.ui index f2cf9b3202..55e5ce0552 100644 --- a/src/calibre/gui2/dialogs/config.ui +++ b/src/calibre/gui2/dialogs/config.ui @@ -8,7 +8,7 @@ 0 0 800 - 557 + 583 @@ -426,6 +426,16 @@
+ + + + Search as you type + + + true + + + @@ -591,15 +601,6 @@ - roman_numerals - groupBox_2 - systray_icon - sync_news - delete_news - separate_cover_flow - systray_notifications - - diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py index 3d5a30ec05..266a878440 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library.py @@ -1095,6 +1095,7 @@ class SearchBox(QLineEdit): QLineEdit.__init__(self, parent) self.help_text = help_text self.initial_state = True + self.as_you_type = True self.default_palette = QApplication.palette(self) self.gray = QPalette(self.default_palette) self.gray.setBrush(QPalette.Text, QBrush(QColor('gray'))) @@ -1123,6 +1124,9 @@ class SearchBox(QLineEdit): if self.initial_state: self.normalize_state() self.initial_state = False + if not self.as_you_type: + if event.key() in (Qt.Key_Return, Qt.Key_Enter): + self.do_search() QLineEdit.keyPressEvent(self, event) def mouseReleaseEvent(self, event): @@ -1132,17 +1136,21 @@ class SearchBox(QLineEdit): QLineEdit.mouseReleaseEvent(self, event) def text_edited_slot(self, text): - text = qstring_to_unicode(text) if isinstance(text, QString) else unicode(text) - self.prev_text = text - self.timer = self.startTimer(self.__class__.INTERVAL) + if self.as_you_type: + text = qstring_to_unicode(text) if isinstance(text, QString) else unicode(text) + self.prev_text = text + self.timer = self.startTimer(self.__class__.INTERVAL) def timerEvent(self, event): self.killTimer(event.timerId()) if event.timerId() == self.timer: - text = qstring_to_unicode(self.text()) - refinement = text.startswith(self.prev_search) and ':' not in text - self.prev_search = text - self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), text, refinement) + self.do_search() + + def do_search(self): + text = qstring_to_unicode(self.text()) + refinement = text.startswith(self.prev_search) and ':' not in text + self.prev_search = text + self.emit(SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), text, refinement) def search_from_tokens(self, tokens, all): ans = u' '.join([u'%s:%s'%x for x in tokens]) @@ -1161,3 +1169,6 @@ class SearchBox(QLineEdit): self.end(False) self.initial_state = False + def search_as_you_type(self, enabled): + self.as_you_type = enabled + diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index cd1e545a29..4384121418 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -148,6 +148,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.system_tray_icon.hide() else: self.system_tray_icon.show() + self.search.search_as_you_type(config['search_as_you_type']) self.system_tray_menu = QMenu(self) self.restore_action = self.system_tray_menu.addAction( QIcon(':/images/page.svg'), _('&Restore')) @@ -1422,6 +1423,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): self.content_server = d.server if d.result() == d.Accepted: self.tool_bar.setIconSize(config['toolbar_icon_size']) + self.search.search_as_you_type(config['search_as_you_type']) self.tool_bar.setToolButtonStyle( Qt.ToolButtonTextUnderIcon if \ config['show_text_in_toolbar'] else \ From 794eba4b46fa0764a6a6dd0b3b1175a34574d39b Mon Sep 17 00:00:00 2001 From: John Schember Date: Wed, 8 Jul 2009 07:55:08 -0400 Subject: [PATCH 31/31] Header and footer removal by regex moved to from pdf input to structure detection in plumber. --- src/calibre/ebooks/conversion/plumber.py | 25 +++++++++ src/calibre/ebooks/conversion/preprocess.py | 25 ++++----- src/calibre/ebooks/pdf/input.py | 10 ---- src/calibre/gui2/convert/pdf_input.py | 32 +----------- src/calibre/gui2/convert/pdf_input.ui | 28 ++-------- .../gui2/convert/structure_detection.py | 18 +++++-- .../gui2/convert/structure_detection.ui | 52 ++++++++++++++++--- 7 files changed, 104 insertions(+), 86 deletions(-) diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index dc6c0f8b52..3c52ec2d7b 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -315,6 +315,31 @@ OptionRecommendation(name='preprocess_html', ) ), +OptionRecommendation(name='remove_header', + recommended_value=False, level=OptionRecommendation.LOW, + help=_('Use a regular expression to try and remove the header.' + ) + ), + +OptionRecommendation(name='header_regex', + recommended_value='(?i)(?<=
)((\s*(()*
\s*)?\d+
\s*.*?\s*)|(\s*(()*
\s*)?.*?
\s*\d+))(?=
)', + level=OptionRecommendation.LOW, + help=_('The regular expression to use to remove the header.' + ) + ), + +OptionRecommendation(name='remove_footer', + recommended_value=False, level=OptionRecommendation.LOW, + help=_('Use a regular expression to try and remove the footer.' + ) + ), + +OptionRecommendation(name='footer_regex', + recommended_value='(?i)(?<=
)((\s*(()*
\s*)?\d+
\s*.*?\s*)|(\s*(()*
\s*)?.*?
\s*\d+))(?=
)', + level=OptionRecommendation.LOW, + help=_('The regular expression to use to remove the footer.' + ) + ), OptionRecommendation(name='read_metadata_from_opf', recommended_value=None, level=OptionRecommendation.LOW, diff --git a/src/calibre/ebooks/conversion/preprocess.py b/src/calibre/ebooks/conversion/preprocess.py index f9788fdba8..69d6f1e511 100644 --- a/src/calibre/ebooks/conversion/preprocess.py +++ b/src/calibre/ebooks/conversion/preprocess.py @@ -185,17 +185,7 @@ class HTMLPreProcessor(object): elif self.is_book_designer(html): rules = self.BOOK_DESIGNER elif self.is_pdftohtml(html): - start_rules = [] end_rules = [] - - if getattr(self.extra_opts, 'remove_header', None): - start_rules.append( - (re.compile(getattr(self.extra_opts, 'header_regex')), lambda match : '') - ) - if getattr(self.extra_opts, 'remove_footer', None): - start_rules.append( - (re.compile(getattr(self.extra_opts, 'footer_regex')), lambda match : '') - ) if getattr(self.extra_opts, 'unwrap_factor', None): length = line_length(html, getattr(self.extra_opts, 'unwrap_factor')) if length: @@ -204,10 +194,21 @@ class HTMLPreProcessor(object): (re.compile(r'(?<=.{%i}[a-z\.,;:)-IA])\s*(?P)?\s*()\s*(?=(<(i|b|u)>)?\s*[\w\d(])' % length, re.UNICODE), wrap_lines), ) - rules = start_rules + self.PDFTOHTML + end_rules + rules = self.PDFTOHTML + end_rules else: rules = [] - for rule in self.PREPROCESS + rules: + + pre_rules = [] + if getattr(self.extra_opts, 'remove_header', None): + pre_rules.append( + (re.compile(getattr(self.extra_opts, 'header_regex')), lambda match : '') + ) + if getattr(self.extra_opts, 'remove_footer', None): + pre_rules.append( + (re.compile(getattr(self.extra_opts, 'footer_regex')), lambda match : '') + ) + + for rule in self.PREPROCESS + pre_rules + rules: html = rule[0].sub(rule[1], html) # Handle broken XHTML w/ SVG (ugh) diff --git a/src/calibre/ebooks/pdf/input.py b/src/calibre/ebooks/pdf/input.py index e17d50869e..58abbd635c 100644 --- a/src/calibre/ebooks/pdf/input.py +++ b/src/calibre/ebooks/pdf/input.py @@ -24,16 +24,6 @@ class PDFInput(InputFormatPlugin): help=_('Scale used to determine the length at which a line should ' 'be unwrapped. Valid values are a decimal between 0 and 1. The ' 'default is 0.5, this is the median line length.')), - OptionRecommendation(name='remove_header', recommended_value=False, - help=_('Use a regular expression to try and remove the header.')), - OptionRecommendation(name='header_regex', - recommended_value='(?i)(?<=
)((\s*(()*
\s*)?\d+
\s*.*?\s*)|(\s*(()*
\s*)?.*?
\s*\d+))(?=
)', - help=_('The regular expression to use to remove the header.')), - OptionRecommendation(name='remove_footer', recommended_value=False, - help=_('Use a regular expression to try and remove the footer.')), - OptionRecommendation(name='footer_regex', - recommended_value='(?i)(?<=
)((\s*(()*
\s*)?\d+
\s*.*?\s*)|(\s*(()*
\s*)?.*?
\s*\d+))(?=
)', - help=_('The regular expression to use to remove the footer.')), ]) def convert(self, stream, options, file_ext, log, diff --git a/src/calibre/gui2/convert/pdf_input.py b/src/calibre/gui2/convert/pdf_input.py index bfd658526c..e4a9541823 100644 --- a/src/calibre/gui2/convert/pdf_input.py +++ b/src/calibre/gui2/convert/pdf_input.py @@ -4,13 +4,8 @@ __license__ = 'GPL 3' __copyright__ = '2009, John Schember ' __docformat__ = 'restructuredtext en' -import re - -from PyQt4.Qt import SIGNAL - from calibre.gui2.convert.pdf_input_ui import Ui_Form from calibre.gui2.convert import Widget -from calibre.gui2 import qstring_to_unicode, error_dialog class PluginWidget(Widget, Ui_Form): @@ -19,31 +14,6 @@ class PluginWidget(Widget, Ui_Form): def __init__(self, parent, get_option, get_help, db=None, book_id=None): Widget.__init__(self, parent, 'pdf_input', - ['no_images', 'unwrap_factor', 'remove_header', 'header_regex', - 'remove_footer', 'footer_regex']) + ['no_images', 'unwrap_factor']) self.db, self.book_id = db, book_id self.initialize_options(get_option, get_help, db, book_id) - - self.opt_header_regex.setEnabled(self.opt_remove_header.isChecked()) - self.opt_footer_regex.setEnabled(self.opt_remove_footer.isChecked()) - - self.connect(self.opt_remove_header, SIGNAL('stateChanged(int)'), self.header_regex_state) - self.connect(self.opt_remove_footer, SIGNAL('stateChanged(int)'), self.footer_regex_state) - - def header_regex_state(self, state): - self.opt_header_regex.setEnabled(state) - - def footer_regex_state(self, state): - self.opt_footer_regex.setEnabled(state) - - def pre_commit_check(self): - for x in ('header_regex', 'footer_regex'): - x = getattr(self, 'opt_'+x) - try: - pat = qstring_to_unicode(x.text()) - re.compile(pat) - except Exception, err: - error_dialog(self, _('Invalid regular expression'), - _('Invalid regular expression: %s')%err).exec_() - return False - return True diff --git a/src/calibre/gui2/convert/pdf_input.ui b/src/calibre/gui2/convert/pdf_input.ui index d34c6d404b..40f480b15d 100644 --- a/src/calibre/gui2/convert/pdf_input.ui +++ b/src/calibre/gui2/convert/pdf_input.ui @@ -14,14 +14,14 @@ Form - + Line Un-Wrapping Factor: - + Qt::Vertical @@ -34,7 +34,7 @@ - + 1.000000000000000 @@ -47,33 +47,13 @@ - + No Images - - - - Remove Header - - - - - - - Remove Footer - - - - - - - - -
diff --git a/src/calibre/gui2/convert/structure_detection.py b/src/calibre/gui2/convert/structure_detection.py index 66dff86aca..ee0a389478 100644 --- a/src/calibre/gui2/convert/structure_detection.py +++ b/src/calibre/gui2/convert/structure_detection.py @@ -6,10 +6,13 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' +import re + +from PyQt4.Qt import SIGNAL from calibre.gui2.convert.structure_detection_ui import Ui_Form from calibre.gui2.convert import Widget -from calibre.gui2 import error_dialog +from calibre.gui2 import error_dialog, qstring_to_unicode class StructureDetectionWidget(Widget, Ui_Form): @@ -23,7 +26,8 @@ class StructureDetectionWidget(Widget, Ui_Form): ['chapter', 'chapter_mark', 'remove_first_image', 'insert_metadata', 'page_breaks_before', - 'preprocess_html'] + 'preprocess_html', 'remove_header', 'header_regex', + 'remove_footer', 'footer_regex'] ) self.db, self.book_id = db, book_id self.initialize_options(get_option, get_help, db, book_id) @@ -31,8 +35,16 @@ class StructureDetectionWidget(Widget, Ui_Form): self.opt_page_breaks_before.set_msg(_('Insert page breaks before ' '(XPath expression):')) - def pre_commit_check(self): + for x in ('header_regex', 'footer_regex'): + x = getattr(self, 'opt_'+x) + try: + pat = qstring_to_unicode(x.text()) + re.compile(pat) + except Exception, err: + error_dialog(self, _('Invalid regular expression'), + _('Invalid regular expression: %s')%err).exec_() + return False for x in ('chapter', 'page_breaks_before'): x = getattr(self, 'opt_'+x) if not x.check(): diff --git a/src/calibre/gui2/convert/structure_detection.ui b/src/calibre/gui2/convert/structure_detection.ui index 768b430c5a..eebc0f0d53 100644 --- a/src/calibre/gui2/convert/structure_detection.ui +++ b/src/calibre/gui2/convert/structure_detection.ui @@ -14,6 +14,9 @@ Form + + + @@ -62,20 +65,27 @@ - + + + + &Footer regular expression: + + + opt_footer_regex + + + + &Preprocess input file to possibly improve structure detection - + - - - - + Qt::Vertical @@ -88,6 +98,36 @@ + + + + &Header regular expression: + + + opt_header_regex + + + + + + + Remove F&ooter + + + + + + + Remove H&eader + + + + + + + + +