From fb768388748aa777139dec2e64ce537940e6c87b Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 31 Dec 2010 06:48:44 +0000 Subject: [PATCH 01/13] Fix display of names in partition mode --- resources/default_tweaks.py | 6 +++--- src/calibre/gui2/tag_view.py | 3 +-- src/calibre/utils/formatter.py | 6 ++++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index a2a9a0a043..9f663a21b4 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -77,9 +77,9 @@ # sort: the sort value. For authors, this is the author_sort for that author # category: the category (e.g., authors, series) that the item is in. categories_collapse_more_than = 50 -categories_collapsed_name_template = '{first.name:shorten(4,'',0)}{last.name::shorten(4,'',0)| - |}' -categories_collapsed_rating_template = '{first.avg_rating:4.2f}{last.avg_rating:4.2f| - |}' -categories_collapsed_popularity_template = '{first.count:d}{last.count:d| - |}' +categories_collapsed_name_template = '{first.name:shorten(4,'',0)} - {last.name::shorten(4,'',0)}' +categories_collapsed_rating_template = '{first.avg_rating:4.2f:ifempty(0)} - {last.avg_rating:4.2f:ifempty(0)}' +categories_collapsed_popularity_template = '{first.count:d} - {last.count:d}' categories_collapse_model = 'first letter' # Set whether boolean custom columns are two- or three-valued. diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index a9ba22f768..247904bb8e 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -674,7 +674,6 @@ def refresh(self, data=None): if data is None: return False row_index = -1 - empty_tag = Tag('') collapse = tweaks['categories_collapse_more_than'] collapse_model = tweaks['categories_collapse_model'] if sort_by == 'name': @@ -726,7 +725,7 @@ def refresh(self, data=None): if cat_len > idx + collapse: d['last'] = data[r][idx+collapse-1] else: - d['last'] = empty_tag + d['last'] = data[r][cat_len-1] name = eval_formatter.safe_format(collapse_template, d, 'TAG_VIEW', None) sub_cat = TagTreeItem(parent=category, diff --git a/src/calibre/utils/formatter.py b/src/calibre/utils/formatter.py index 7587a334e8..4fe8ad2e4f 100644 --- a/src/calibre/utils/formatter.py +++ b/src/calibre/utils/formatter.py @@ -371,6 +371,12 @@ def get_value(self, key, args, kwargs): raise Exception('get_value must be implemented in the subclass') def format_field(self, val, fmt): + # ensure we are dealing with a string. + if isinstance(val, (int, float)): + if val: + val = unicode(val) + else: + val = '' # Handle conditional text fmt, prefix, suffix = self._explode_format_string(fmt) From 08ad0576912130cefa7c98fb1efefcbe87877dba Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 31 Dec 2010 07:58:27 +0000 Subject: [PATCH 02/13] Improvements to tag browser search. Correction to HistoryLineEdit (possibly wrong) to refresh the items in the combo box. --- src/calibre/gui2/tag_view.py | 42 ++++++++++++++++++------------------ src/calibre/gui2/widgets.py | 5 ++++- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 247904bb8e..0d1be3be28 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -11,14 +11,13 @@ from functools import partial from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \ - QIcon, QPoint, QVBoxLayout, QHBoxLayout, QComboBox,\ - QAbstractItemModel, QVariant, QModelIndex, QMenu, \ - QPushButton, QWidget, QItemDelegate, QString + QIcon, QPoint, QVBoxLayout, QHBoxLayout, QComboBox, QTimer,\ + QAbstractItemModel, QVariant, QModelIndex, QMenu, QFrame,\ + QPushButton, QWidget, QItemDelegate, QString, QLabel from calibre.ebooks.metadata import title_sort from calibre.gui2 import config, NONE from calibre.library.field_metadata import TagsIcons, category_icon_map -from calibre.library.database2 import Tag from calibre.utils.config import tweaks from calibre.utils.icu import sort_key, upper, lower from calibre.utils.search_query_parser import saved_searches @@ -1179,6 +1178,20 @@ def __init__(self, parent): parent.tags_view = TagsView(parent) self.tags_view = parent.tags_view self._layout.addWidget(parent.tags_view) + l = QLabel(self.tags_view) + l.setFrameStyle(QFrame.StyledPanel) + l.setAutoFillBackground(True) + l.setText(_('No More Matches')) + l.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) + l.resize(l.size() + QSize(20, 20)) + l.move(20,20) + l.setVisible(False) + self.not_found_label = l + + self.label_timer = QTimer() + self.label_timer.setSingleShot(True) + self.label_timer.timeout.connect(self.timer_event, type=Qt.QueuedConnection) + parent.sort_by = QComboBox(parent) # Must be in the same order as db2.CATEGORY_SORTS @@ -1234,20 +1247,6 @@ def find(self): if not txt: return - self.item_search.blockSignals(True) - self.item_search.lineEdit().blockSignals(True) - self.search_button.setFocus(True) - idx = self.item_search.findText(txt, Qt.MatchFixedString) - if idx < 0: - self.item_search.insertItem(0, txt) - else: - t = self.item_search.itemText(idx) - self.item_search.removeItem(idx) - self.item_search.insertItem(0, t) - self.item_search.setCurrentIndex(0) - self.item_search.blockSignals(False) - self.item_search.lineEdit().blockSignals(False) - colon = txt.find(':') key = None if colon > 0: @@ -1259,10 +1258,11 @@ def find(self): if self.current_position: model.show_item_at_index(self.current_position, box=True) elif self.item_search.text(): - warning_dialog(self.tags_view, _('No item found'), - _('No (more) matches for that search')).exec_() - + self.not_found_label.setVisible(True) + self.label_timer.start(1000) + def timer_event(self): + self.not_found_label.setVisible(False) # }}} diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index b2d8e4b8fd..633e7d4333 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -551,7 +551,10 @@ def save_history(self): item = unicode(self.itemText(i)) if item not in items: items.append(item) - + self.blockSignals(True) + self.clear() + self.addItems(items) + self.blockSignals(False) history.set(self.store_name, items) def setText(self, t): From 47168fa3f3721967c6bbcefe35d7e1369a4983aa Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 31 Dec 2010 11:43:13 +0000 Subject: [PATCH 03/13] Clean up search code. Add comments. Add shortcuts. Make history work better. --- src/calibre/gui2/tag_view.py | 115 +++++++++++++++++++++-------------- src/calibre/gui2/widgets.py | 1 + 2 files changed, 71 insertions(+), 45 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 0d1be3be28..e609a26a4b 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -13,16 +13,17 @@ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QFont, QSize, \ QIcon, QPoint, QVBoxLayout, QHBoxLayout, QComboBox, QTimer,\ QAbstractItemModel, QVariant, QModelIndex, QMenu, QFrame,\ - QPushButton, QWidget, QItemDelegate, QString, QLabel + QPushButton, QWidget, QItemDelegate, QString, QLabel, \ + QShortcut, QKeySequence, SIGNAL from calibre.ebooks.metadata import title_sort from calibre.gui2 import config, NONE from calibre.library.field_metadata import TagsIcons, category_icon_map from calibre.utils.config import tweaks -from calibre.utils.icu import sort_key, upper, lower +from calibre.utils.icu import sort_key, upper, lower, strcmp from calibre.utils.search_query_parser import saved_searches from calibre.utils.formatter import eval_formatter -from calibre.gui2 import error_dialog, warning_dialog +from calibre.gui2 import error_dialog from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.tag_categories import TagCategories from calibre.gui2.dialogs.tag_list_editor import TagListEditor @@ -326,11 +327,7 @@ def recount(self, *args): path = None except: #Database connection could be closed if an integrity check is happening pass - if path: - idx = self.model().index_for_path(path) - if idx.isValid(): - self.setCurrentIndex(idx) - self.scrollTo(idx, QTreeView.PositionAtCenter) + self._model.show_item_at_path(path) # If the number of user categories changed, if custom columns have come or # gone, or if columns have been hidden or restored, we must rebuild the @@ -800,11 +797,7 @@ def setData(self, index, value, role=Qt.EditRole): self.tags_view.tag_item_renamed.emit() item.tag.name = val self.refresh() # Should work, because no categories can have disappeared - if path: - idx = self.index_for_path(path) - if idx.isValid(): - self.tags_view.setCurrentIndex(idx) - self.tags_view.scrollTo(idx, QTreeView.PositionAtCenter) + self.show_item_at_path(path) return True def headerData(self, *args): @@ -932,7 +925,8 @@ def tokens(self): if self.hidden_categories and self.categories[i] in self.hidden_categories: continue row_index += 1 - if key.endswith(':'): # User category, so skip it. The tag will be marked in its real category + if key.endswith(':'): + # User category, so skip it. The tag will be marked in its real category continue category_item = self.root_item.children[row_index] for tag_item in category_item.child_tags(): @@ -950,13 +944,18 @@ def tokens(self): ans.append('%s%s:"=%s"'%(prefix, category, tag.name)) return ans - def find_node(self, key, txt, start_index): + def find_node(self, key, txt, start_path): + ''' + Search for an item (a node) in the tags browser list that matches both + the key (exact case-insensitive match) and txt (contains case- + insensitive match). Returns the path to the node. + ''' if not txt: return None txt = lower(txt) - if start_index is None or not start_index.isValid(): - start_index = QModelIndex() - self.node_found = None + self.path_found = None + if start_path is None: + start_path = [] def process_tag(depth, tag_index, tag_item, start_path): path = self.path_for_index(tag_index) @@ -966,7 +965,7 @@ def process_tag(depth, tag_index, tag_item, start_path): if tag is None: return False if lower(tag.name).find(txt) >= 0: - self.node_found = tag_index + self.path_found = path return True return False @@ -977,7 +976,7 @@ def process_level(depth, category_index, start_path): return False if path[depth] > start_path[depth]: start_path = path - if key and category_index.internalPointer().category_key != key: + if key and strcmp(category_index.internalPointer().category_key, key) != 0: return False for j in xrange(self.rowCount(category_index)): tag_index = self.index(j, 0, category_index) @@ -991,21 +990,32 @@ def process_level(depth, category_index, start_path): return False for i in xrange(self.rowCount(QModelIndex())): - if process_level(0, self.index(i, 0, QModelIndex()), - self.path_for_index(start_index)): + if process_level(0, self.index(i, 0, QModelIndex()), start_path): break - return self.node_found + return self.path_found + + def show_item_at_path(self, path, box=False): + ''' + Scroll the browser and open categories to show the item referenced by + path. If possible, the item is placed in the center. If box=True, a + box is drawn around the item. + ''' + if path: + self.show_item_at_index(self.index_for_path(path), box) def show_item_at_index(self, idx, box=False): if idx.isValid(): - tag_item = idx.internalPointer() self.tags_view.setCurrentIndex(idx) self.tags_view.scrollTo(idx, QTreeView.PositionAtCenter) if box: + tag_item = idx.internalPointer() tag_item.boxed = True self.dataChanged.emit(idx, idx) def clear_boxed(self): + ''' + Clear all boxes around items. + ''' def process_tag(tag_index, tag_item): if tag_item.boxed: tag_item.boxed = False @@ -1146,14 +1156,15 @@ def __init__(self, parent): self.setLayout(self._layout) self._layout.setContentsMargins(0,0,0,0) + # Set up the find box & button search_layout = QHBoxLayout() self._layout.addLayout(search_layout) self.item_search = HistoryLineEdit(parent) try: - self.item_search.lineEdit().setPlaceholderText(_('Find item in tag browser')) + self.item_search.lineEdit().setPlaceholderText( + _('Find item in tag browser')) except: - # Using Qt < 4.7 - pass + pass # Using Qt < 4.7 self.item_search.setToolTip(_( 'Search for items. This is a "contains" search; items containing the\n' 'text anywhere in the name will be found. You can limit the search\n' @@ -1162,12 +1173,16 @@ def __init__(self, parent): '*foo will filter all categories at once, showing only those items\n' 'containing the text "foo"')) search_layout.addWidget(self.item_search) + # Not sure if the shortcut should be translatable ... + sc = QShortcut(QKeySequence(_('ALT+f')), parent) + sc.connect(sc, SIGNAL('activated()'), self.set_focus_to_find_box) + self.search_button = QPushButton() - self.search_button.setText(_('&Find')) + self.search_button.setText(_('F&ind')) self.search_button.setToolTip(_('Find the first/next matching item')) self.search_button.setFixedWidth(40) search_layout.addWidget(self.search_button) - self.current_position = None + self.current_find_position = None self.search_button.clicked.connect(self.find) self.item_search.initialize('tag_browser_search') self.item_search.lineEdit().returnPressed.connect(self.do_find) @@ -1178,20 +1193,21 @@ def __init__(self, parent): parent.tags_view = TagsView(parent) self.tags_view = parent.tags_view self._layout.addWidget(parent.tags_view) + + # Now the floating 'not found' box l = QLabel(self.tags_view) + self.not_found_label = l l.setFrameStyle(QFrame.StyledPanel) l.setAutoFillBackground(True) l.setText(_('No More Matches')) l.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) l.resize(l.size() + QSize(20, 20)) - l.move(20,20) + l.move(10,20) l.setVisible(False) - self.not_found_label = l - - self.label_timer = QTimer() - self.label_timer.setSingleShot(True) - self.label_timer.timeout.connect(self.timer_event, type=Qt.QueuedConnection) - + self.not_found_label_timer = QTimer() + self.not_found_label_timer.setSingleShot(True) + self.not_found_label_timer.timeout.connect(self.not_found_label_timer_event, + type=Qt.QueuedConnection) parent.sort_by = QComboBox(parent) # Must be in the same order as db2.CATEGORY_SORTS @@ -1224,10 +1240,14 @@ def set_pane_is_visible(self, to_what): self.tags_view.set_pane_is_visible(to_what) def find_text_changed(self, str): - self.current_position = None + self.current_find_position = None + + def set_focus_to_find_box(self): + self.item_search.setFocus() + self.item_search.lineEdit().selectAll() def do_find(self, str=None): - self.current_position = None + self.current_find_position = None self.find() def find(self): @@ -1237,16 +1257,20 @@ def find(self): if txt.startswith('*'): self.tags_view.set_new_model(filter_categories_by=txt[1:]) - self.current_position = None + self.current_find_position = None return if model.get_filter_categories_by(): self.tags_view.set_new_model(filter_categories_by=None) - self.current_position = None + self.current_find_position = None model = self.tags_view.model() if not txt: return + self.item_search.lineEdit().blockSignals(True) + self.search_button.setFocus(True) + self.item_search.lineEdit().blockSignals(False) + colon = txt.find(':') key = None if colon > 0: @@ -1254,14 +1278,15 @@ def find(self): field_metadata.search_term_to_field_key(txt[:colon]) txt = txt[colon+1:] - self.current_position = model.find_node(key, txt, self.current_position) - if self.current_position: - model.show_item_at_index(self.current_position, box=True) + self.current_find_position = model.find_node(key, txt, + self.current_find_position) + if self.current_find_position: + model.show_item_at_path(self.current_find_position, box=True) elif self.item_search.text(): self.not_found_label.setVisible(True) - self.label_timer.start(1000) + self.not_found_label_timer.start(1000) - def timer_event(self): + def not_found_label_timer_event(self): self.not_found_label.setVisible(False) # }}} diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 633e7d4333..bc3c23876f 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -554,6 +554,7 @@ def save_history(self): self.blockSignals(True) self.clear() self.addItems(items) + self.setEditText(ct) self.blockSignals(False) history.set(self.store_name, items) From c40d78a53f54b7d4ee150c7aed03466143297557 Mon Sep 17 00:00:00 2001 From: John Schember Date: Fri, 31 Dec 2010 08:59:44 -0500 Subject: [PATCH 04/13] TCR Output: Completely rewrite TCR compression. Remove TCR compression level. Fix issue with TCR compression adding junk to the end of the the text. TCR compression now takes seconds to compress instead of tens of minutes. TCR compression now compresses to a smaller size than previous compression algorithm. --- src/calibre/ebooks/compression/tcr.py | 210 ++++++++++++++------------ src/calibre/ebooks/tcr/output.py | 7 +- 2 files changed, 112 insertions(+), 105 deletions(-) diff --git a/src/calibre/ebooks/compression/tcr.py b/src/calibre/ebooks/compression/tcr.py index 40bed613ec..b8dd4e9afd 100644 --- a/src/calibre/ebooks/compression/tcr.py +++ b/src/calibre/ebooks/compression/tcr.py @@ -6,11 +6,118 @@ import re +class TCRCompressor(object): + ''' + TCR compression takes the form header+code_dict+coded_text. + The header is always "!!8-Bit!!". The code dict is a list of 256 strings. + The list takes the form 1 byte length and then a string. Each position in + The list corresponds to a code found in the file. The coded text is + string of characters values. for instance the character Q represents the + value 81 which corresponds to the string in the code list at position 81. + ''' + + def _reset(self): + # List of indexes in the codes list that are empty and can hold new codes + self.unused_codes = set() + self.coded_txt = '' + # Generate initial codes from text. + # The index of the list will be the code that represents the characters at that location + # in the list + self.codes = [] + + def _combine_codes(self): + ''' + Combine two codes that always appear in pair into a single code. + The intent is to create more unused codes. + ''' + possible_codes = [] + a_code = set(re.findall('(?msu).', self.coded_txt)) + + for code in a_code: + single_code = set(re.findall('(?msu)%s.' % re.escape(code), self.coded_txt)) + if len(single_code) == 1: + possible_codes.append(single_code.pop()) + + for code in possible_codes: + self.coded_txt = self.coded_txt.replace(code, code[0]) + self.codes[ord(code[0])] = '%s%s' % (self.codes[ord(code[0])], self.codes[ord(code[1])]) + + def _free_unused_codes(self): + ''' + Look for codes that do no not appear in the coded text and add them to + the list of free codes. + ''' + for i in xrange(256): + if i not in self.unused_codes: + if chr(i) not in self.coded_txt: + self.unused_codes.add(i) + + def _new_codes(self): + ''' + Create new codes from codes that occur in pairs often. + ''' + possible_new_codes = list(set(re.findall('(?msu)..', self.coded_txt))) + new_codes_count = [] + + for c in possible_new_codes: + count = self.coded_txt.count(c) + # Less than 3 occurrences will not produce any size reduction. + if count > 2: + new_codes_count.append((c, count)) + + # Arrange the codes in order of least to most occurring. + possible_new_codes = [x[0] for x in sorted(new_codes_count, key=lambda c: c[1])] + + return possible_new_codes + + def compress(self, txt): + self._reset() + + self.codes = list(set(re.findall('(?msu).', txt))) + + # Replace the text with their corresponding code + for c in txt: + self.coded_txt += chr(self.codes.index(c)) + + # Zero the unused codes and record which are unused. + for i in range(len(self.codes), 256): + self.codes.append('') + self.unused_codes.add(i) + + self._combine_codes() + possible_codes = self._new_codes() + + while possible_codes and self.unused_codes: + while possible_codes and self.unused_codes: + unused_code = self.unused_codes.pop() + # Take the last possible codes and split it into individual + # codes. The last possible code is the most often occurring. + code1, code2 = possible_codes.pop() + self.codes[unused_code] = '%s%s' % (self.codes[ord(code1)], self.codes[ord(code2)]) + self.coded_txt = self.coded_txt.replace('%s%s' % (code1, code2), chr(unused_code)) + self._combine_codes() + self._free_unused_codes() + possible_codes = self._new_codes() + + self._free_unused_codes() + + # Generate the code dictionary. + code_dict = [] + for i in xrange(0, 256): + if i in self.unused_codes: + code_dict.append(chr(0)) + else: + code_dict.append(chr(len(self.codes[i])) + self.codes[i]) + + # Join the identifier with the dictionary and coded text. + return '!!8-Bit!!'+''.join(code_dict)+self.coded_txt + + def decompress(stream): txt = [] stream.seek(0) if stream.read(9) != '!!8-Bit!!': - raise ValueError('File %s contaions an invalid TCR header.' % stream.name) + raise ValueError('File %s contains an invalid TCR header.' % stream.name) # Codes that the file contents are broken down into. entries = [] @@ -26,101 +133,6 @@ def decompress(stream): return ''.join(txt) - -def compress(txt, level=5): - ''' - TCR compression takes the form header+code_list+coded_text. - The header is always "!!8-Bit!!". The code list is a list of 256 strings. - The list takes the form 1 byte length and then a string. Each position in - The list corresponds to a code found in the file. The coded text is - string of characters vaules. for instance the character Q represents the - value 81 which corresponds to the string in the code list at position 81. - ''' - # Turn each unique character into a coded value. - # The code of the string at a given position are represented by the position - # they occupy in the list. - codes = list(set(re.findall('(?msu).', txt))) - for i in range(len(codes), 256): - codes.append('') - # Set the compression level. - if level <= 1: - new_length = 256 - if level >= 10: - new_length = 1 - else: - new_length = int(256 * (10 - level) * .1) - new_length = 1 if new_length < 1 else new_length - # Replace txt with codes. - coded_txt = '' - for c in txt: - coded_txt += chr(codes.index(c)) - txt = coded_txt - # Start compressing the text. - new = True - merged = True - while new or merged: - # Merge codes that always follow another code - merge = [] - merged = False - for i in xrange(256): - if codes[i] != '': - # Find all codes that are next to i. - fall = list(set(re.findall('(?msu)%s.' % re.escape(chr(i)), txt))) - # 1 if only one code comes after i. - if len(fall) == 1: - # We are searching codes and each code is always 1 character. - j = ord(fall[0][1:2]) - # Only merge if the total length of the string represented by - # code is less than 256. - if len(codes[i]) + len(codes[j]) < 256: - merge.append((i, j)) - if merge: - merged = True - for i, j in merge: - # Merge the string for j into the string for i. - if i == j: - # Don't use += here just in case something goes wrong. This - # will prevent out of control memory consumption. This is - # unecessary but when creating this routine it happened due - # to an error. - codes[i] = codes[i] + codes[i] - else: - codes[i] = codes[i] + codes[j] - txt = txt.replace(chr(i)+chr(j), chr(i)) - if chr(j) not in txt: - codes[j] = '' - new = False - if '' in codes: - # Create a list of codes based on combinations of codes that are next - # to each other. The amount of savings for the new code is calculated. - new_codes = [] - for c in list(set(re.findall('(?msu)..', txt))): - i = ord(c[0:1]) - j = ord(c[1:2]) - if codes[i]+codes[j] in codes: - continue - savings = txt.count(chr(i)+chr(j)) - len(codes[i]) - len(codes[j]) - if savings > 2 and len(codes[i]) + len(codes[j]) < 256: - new_codes.append((savings, i, j, codes[i], codes[j])) - if new_codes: - new = True - # Sort the codes from highest savings to lowest. - new_codes.sort(lambda x, y: -1 if x[0] > y[0] else 1 if x[0] < y[0] else 0) - # The shorter new_length the more chances time merging will happen - # giving more changes for better codes to be created. However, - # the shorter new_lengh the longer it will take to compress. - new_codes = new_codes[:new_length] - for code in new_codes: - if '' not in codes: - break - c = codes.index('') - codes[c] = code[3]+code[4] - txt = txt.replace(chr(code[1])+chr(code[2]), chr(c)) - # Generate the code dictionary. - header = [] - for code in codes: - header.append(chr(len(code))+code) - for i in xrange(len(header), 256): - header.append(chr(0)) - # Join the identifier with the dictionary and coded text. - return '!!8-Bit!!'+''.join(header)+txt +def compress(txt): + t = TCRCompressor() + return t.compress(txt) diff --git a/src/calibre/ebooks/tcr/output.py b/src/calibre/ebooks/tcr/output.py index 603d35d099..3ca82730cc 100644 --- a/src/calibre/ebooks/tcr/output.py +++ b/src/calibre/ebooks/tcr/output.py @@ -22,11 +22,6 @@ class TCROutput(OutputFormatPlugin): level=OptionRecommendation.LOW, help=_('Specify the character encoding of the output document. ' \ 'The default is utf-8.')), - OptionRecommendation(name='compression_level', recommended_value=5, - level=OptionRecommendation.LOW, - help=_('Specify the compression level to use. Scale 1 - 10. 1 ' \ - 'being the lowest compression but the fastest and 10 being the ' \ - 'highest compression but the slowest.')), ]) def convert(self, oeb_book, output_path, input_plugin, opts, log): @@ -48,7 +43,7 @@ def convert(self, oeb_book, output_path, input_plugin, opts, log): txt = writer.extract_content(oeb_book, opts).encode(opts.output_encoding, 'replace') log.info('Compressing text...') - txt = compress(txt, opts.compression_level) + txt = compress(txt) out_stream.seek(0) out_stream.truncate() From e98f14292a25f572c6edf6e163f9f70c8defebba Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 31 Dec 2010 14:18:05 +0000 Subject: [PATCH 05/13] Add the possibility to make an 'all by something' collection. Syntax: abs:collection_name. If there is an entry in the tweak sony_sort_collections_by, then that controls the sorting, otherwise the collection name is assumed to be a field. Sorts by title_sort within sort_field --- src/calibre/devices/usbms/books.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index 1e7d74480a..8c92aa8a6e 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -140,11 +140,19 @@ def get_collections(self, collection_attributes): all_by_author = '' all_by_title = '' ca = [] + all_by_something = [] for c in collection_attributes: - if c.startswith('aba:') and c[4:]: + if c.startswith('aba:') and c[4:].strip(): all_by_author = c[4:].strip() - elif c.startswith('abt:') and c[4:]: + elif c.startswith('abt:') and c[4:].strip(): all_by_title = c[4:].strip() + elif c.startswith('abs:') and c[4:].strip(): + name = c[4:].strip() + sby = self.in_category_sort_rules(name) + if sby is None: + sby = name + if name and sby: + all_by_something.append((name, sby)) else: ca.append(c.lower()) collection_attributes = ca @@ -251,6 +259,10 @@ def get_collections(self, collection_attributes): if all_by_title not in collections: collections[all_by_title] = {} collections[all_by_title][lpath] = (book, tsval, asval) + for (n, sb) in all_by_something: + if n not in collections: + collections[n] = {} + collections[n][lpath] = (book, book.get(sb, ''), tsval) # Sort collections result = {} From c58f3ff49b9c9822dc2fe83b94a1aad5d0d95df7 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 31 Dec 2010 15:54:23 +0000 Subject: [PATCH 06/13] Change explanatory comment for find_node --- src/calibre/gui2/tag_view.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index e609a26a4b..846ee064cd 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -948,7 +948,10 @@ def find_node(self, key, txt, start_path): ''' Search for an item (a node) in the tags browser list that matches both the key (exact case-insensitive match) and txt (contains case- - insensitive match). Returns the path to the node. + insensitive match). Returns the path to the node. Note that paths are to + a location (second item, fourth item, 25 item), not to a node. If the + tree is changed subsequent to calling this method, the path can easily + refer to a different node or no node at all. ''' if not txt: return None From 1ae29e94c9e36e7240a3a991f09df9993f488ee2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 31 Dec 2010 09:03:08 -0700 Subject: [PATCH 07/13] ... --- src/calibre/gui2/wizard/send_email.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/wizard/send_email.py b/src/calibre/gui2/wizard/send_email.py index 20e73fabe2..b9b65dc940 100644 --- a/src/calibre/gui2/wizard/send_email.py +++ b/src/calibre/gui2/wizard/send_email.py @@ -144,8 +144,10 @@ def create_service_relay(self, service, *args): bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) bb.accepted.connect(d.accept) bb.rejected.connect(d.reject) - d.tl = QLabel('

'+_('You can sign up for a free {name} email ' - 'account at http://{url}. {extra}').format( + d.tl = QLabel(('

'+_('Setup sending email using') + + ' {name}

' + + _('If you don\'t have an account, you can sign up for a free {name} email ' + 'account at http://{url}. {extra}')).format( **service)) l.addWidget(d.tl, 0, 0, 3, 0) d.tl.setWordWrap(True) From f551120364a3fa402a2b81f142558d468f705f31 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 31 Dec 2010 16:09:01 +0000 Subject: [PATCH 08/13] More comment changes --- src/calibre/gui2/tag_view.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 846ee064cd..5e9a5c7b49 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -949,9 +949,10 @@ def find_node(self, key, txt, start_path): Search for an item (a node) in the tags browser list that matches both the key (exact case-insensitive match) and txt (contains case- insensitive match). Returns the path to the node. Note that paths are to - a location (second item, fourth item, 25 item), not to a node. If the - tree is changed subsequent to calling this method, the path can easily - refer to a different node or no node at all. + a location (second item, fourth item, 25 item), not to a node. If + start_path is None, the search starts with the topmost node. If the tree + is changed subsequent to calling this method, the path can easily refer + to a different node or no node at all. ''' if not txt: return None From 0db9b9bd0e2565af34894c461691db7afd9449f8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 31 Dec 2010 10:39:13 -0700 Subject: [PATCH 09/13] ... --- src/calibre/gui2/tag_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 6914634d94..bbc573371a 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -1203,7 +1203,7 @@ def __init__(self, parent): self.not_found_label = l l.setFrameStyle(QFrame.StyledPanel) l.setAutoFillBackground(True) - l.setText('

'+_('No More Matches.

Click Find again to go to first match')) + l.setText('

'+_('No More Matches.

Click Find again to go to first match')) l.setAlignment(Qt.AlignVCenter) l.setWordWrap(True) l.resize(l.sizeHint()) From b3027e671603ffe954e82bc88d7b987823bdaa2c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 31 Dec 2010 10:47:05 -0700 Subject: [PATCH 10/13] ... --- src/calibre/gui2/tag_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index bbc573371a..b3e927875c 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -1289,7 +1289,7 @@ def find(self): model.show_item_at_path(self.current_find_position, box=True) elif self.item_search.text(): self.not_found_label.setVisible(True) - width = self.not_found_label.parent().width()-8 + width = self.width()-8 height = self.not_found_label.heightForWidth(width) + 20 self.not_found_label.resize(width, height) self.not_found_label.move(4, 10) From 4c5c7720d4a1ab4cddd20fd0962c2f5dd8c60b4f Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 31 Dec 2010 17:56:00 +0000 Subject: [PATCH 11/13] Resize not found message --- src/calibre/gui2/tag_view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 6914634d94..e88dbd8927 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -1289,10 +1289,10 @@ def find(self): model.show_item_at_path(self.current_find_position, box=True) elif self.item_search.text(): self.not_found_label.setVisible(True) - width = self.not_found_label.parent().width()-8 + width = self.item_search.width() height = self.not_found_label.heightForWidth(width) + 20 self.not_found_label.resize(width, height) - self.not_found_label.move(4, 10) + self.not_found_label.move(self.search_button.width()/3, 10) self.not_found_label_timer.start(2000) def not_found_label_timer_event(self): From 5a1cf7d87956e0e734584b18d7341688acc4590a Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 31 Dec 2010 18:13:58 +0000 Subject: [PATCH 12/13] Try again with the width of the not found box --- src/calibre/gui2/tag_view.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index e88dbd8927..bdaa9bba9b 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -1289,10 +1289,14 @@ def find(self): model.show_item_at_path(self.current_find_position, box=True) elif self.item_search.text(): self.not_found_label.setVisible(True) - width = self.item_search.width() + if self.tags_view.verticalScrollBar().isVisible(): + sbw = self.tags_view.verticalScrollBar().width() + else: + sbw = 0 + width = self.width() - 8 - sbw height = self.not_found_label.heightForWidth(width) + 20 self.not_found_label.resize(width, height) - self.not_found_label.move(self.search_button.width()/3, 10) + self.not_found_label.move(4, 10) self.not_found_label_timer.start(2000) def not_found_label_timer_event(self): From ac38246909ffefaca293473b8266183c15956ae0 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Fri, 31 Dec 2010 19:43:47 +0000 Subject: [PATCH 13/13] Add another method to the custom data API. get_ids_for_custom_book_data('name') returns all books with data named 'name' --- src/calibre/library/database2.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index cd3c44387b..0a0d322ab5 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -2728,6 +2728,10 @@ def delete_custom_book_data(self, book_id, name): (book_id, name)) self.commit() + def get_ids_for_custom_book_data(self, name): + s = self.conn.get('''SELECT book FROM books_plugin_data WHERE name=?''', (name,)) + return [x[0] for x in s] + def get_custom_recipes(self): for id, title, script in self.conn.get('SELECT id,title,script FROM feeds'): yield id, title, script