diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index 7eafc5b357..9726ed3b09 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -77,9 +77,9 @@ categories_use_field_for_author_name = 'author' # 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/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 @@ class CollectionsBookList(BookList): 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 @@ class CollectionsBookList(BookList): 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 = {} 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 @@ __docformat__ = 'restructuredtext en' 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 @@ class TCROutput(OutputFormatPlugin): 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() diff --git a/src/calibre/gui2/catalog/catalog_csv_xml.py b/src/calibre/gui2/catalog/catalog_csv_xml.py index eeca5f7f72..18f2c210dc 100644 --- a/src/calibre/gui2/catalog/catalog_csv_xml.py +++ b/src/calibre/gui2/catalog/catalog_csv_xml.py @@ -6,11 +6,9 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import os from calibre.gui2 import gprefs from calibre.gui2.catalog.catalog_csv_xml_ui import Ui_Form -from calibre.library.database2 import LibraryDatabase2 -from calibre.utils.config import prefs +from calibre.library import db as db_ from PyQt4.Qt import QWidget, QListWidgetItem class PluginWidget(QWidget, Ui_Form): @@ -30,8 +28,7 @@ class PluginWidget(QWidget, Ui_Form): self.all_fields.append(x) QListWidgetItem(x, self.db_fields) - dbpath = os.path.abspath(prefs['library_path']) - db = LibraryDatabase2(dbpath) + db = db_() for x in sorted(db.custom_field_keys()): self.all_fields.append(x) QListWidgetItem(x, self.db_fields) diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index e3cb8ab5fe..7a35fdb3c2 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -97,7 +97,7 @@ class PluginWidget(QWidget,Ui_Form): #self.read_source_field.setCurrentIndex(index) getattr(self,c_name).setCurrentIndex(index) elif c_type in ['line_edit']: - getattr(self, c_name).setText(opt_value) + getattr(self, c_name).setText(opt_value if opt_value else '') elif c_type in ['radio_button'] and opt_value is not None: getattr(self, c_name).setChecked(opt_value) elif c_type in ['spin_box']: @@ -145,11 +145,11 @@ class PluginWidget(QWidget,Ui_Form): if c_type in ['check_box', 'radio_button']: opt_value = getattr(self, c_name).isChecked() elif c_type in ['combo_box']: - opt_value = unicode(getattr(self,c_name).currentText()) + opt_value = unicode(getattr(self,c_name).currentText()).strip() elif c_type in ['line_edit']: - opt_value = unicode(getattr(self, c_name).text()) + opt_value = unicode(getattr(self, c_name).text()).strip() elif c_type in ['spin_box']: - opt_value = unicode(getattr(self, c_name).cleanText()) + opt_value = unicode(getattr(self, c_name).value()) gprefs.set(self.name + '_' + c_name, opt_value) # Construct opts object diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui index 63ccb912c5..7a2a86c690 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -44,35 +44,35 @@ - Books by Title + Books by &Title - Books by Series + Books by &Series - Recently Added + Recently &Added - Books by Genre + Books by &Genre - Descriptions + &Descriptions @@ -133,7 +133,7 @@ p, li { white-space: pre-wrap; } - Tags to exclude + Tags to &exclude Qt::AutoText @@ -144,6 +144,9 @@ p, li { white-space: pre-wrap; } true + + exclude_genre + @@ -214,7 +217,7 @@ p, li { white-space: pre-wrap; } - Tags to exclude + Tags to &exclude Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter @@ -222,6 +225,9 @@ p, li { white-space: pre-wrap; } true + + exclude_tags + @@ -261,7 +267,7 @@ p, li { white-space: pre-wrap; } - Column/value + &Column/value Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter @@ -269,6 +275,9 @@ p, li { white-space: pre-wrap; } true + + exclude_source_field + @@ -349,7 +358,7 @@ p, li { white-space: pre-wrap; } - Column/value + &Column/value Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter @@ -357,6 +366,9 @@ p, li { white-space: pre-wrap; } true + + read_source_field + @@ -443,7 +455,7 @@ p, li { white-space: pre-wrap; } - Wishlist tag + &Wishlist tag Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter @@ -451,6 +463,9 @@ p, li { white-space: pre-wrap; } true + + wishlist_tag + @@ -479,7 +494,7 @@ p, li { white-space: pre-wrap; } - Thumbnail width + &Thumbnail width Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter @@ -487,6 +502,9 @@ p, li { white-space: pre-wrap; } true + + thumb_width + @@ -501,7 +519,7 @@ p, li { white-space: pre-wrap; } Size hint for Description cover thumbnails - " + inch 2 @@ -545,11 +563,14 @@ p, li { white-space: pre-wrap; } - Description note + &Description note Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + header_note_source_field + @@ -590,11 +611,14 @@ p, li { white-space: pre-wrap; } - Merge with Comments + &Merge with Comments Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + merge_source_field + @@ -623,7 +647,7 @@ p, li { white-space: pre-wrap; } Merge additional content before Comments - Before + &Before @@ -633,7 +657,7 @@ p, li { white-space: pre-wrap; } Merge additional content after Comments - After + &After @@ -650,7 +674,7 @@ p, li { white-space: pre-wrap; } Separate Comments and additional content with horizontal rule - <hr /> + &Separator diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index a9ba22f768..bdaa9bba9b 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -11,19 +11,19 @@ from itertools import izip 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, \ + 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.library.database2 import Tag 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 @@ -327,11 +327,7 @@ class TagsView(QTreeView): # {{{ 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 @@ -674,7 +670,6 @@ class TagsModel(QAbstractItemModel): # {{{ 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 +721,7 @@ class TagsModel(QAbstractItemModel): # {{{ 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, @@ -802,11 +797,7 @@ class TagsModel(QAbstractItemModel): # {{{ 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): @@ -934,7 +925,8 @@ class TagsModel(QAbstractItemModel): # {{{ 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(): @@ -952,13 +944,22 @@ class TagsModel(QAbstractItemModel): # {{{ 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. Note that paths are to + 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 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) @@ -968,7 +969,7 @@ class TagsModel(QAbstractItemModel): # {{{ 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 @@ -979,7 +980,7 @@ class TagsModel(QAbstractItemModel): # {{{ 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) @@ -993,21 +994,32 @@ class TagsModel(QAbstractItemModel): # {{{ 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 @@ -1148,14 +1160,15 @@ class TagBrowserWidget(QWidget): # {{{ 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' @@ -1164,12 +1177,16 @@ class TagBrowserWidget(QWidget): # {{{ '*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) @@ -1181,6 +1198,22 @@ class TagBrowserWidget(QWidget): # {{{ 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.

Click Find again to go to first match')) + l.setAlignment(Qt.AlignVCenter) + l.setWordWrap(True) + l.resize(l.sizeHint()) + l.move(10,20) + l.setVisible(False) + 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 for x in (_('Sort by name'), _('Sort by popularity'), @@ -1212,10 +1245,14 @@ class TagBrowserWidget(QWidget): # {{{ 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): @@ -1225,28 +1262,18 @@ class TagBrowserWidget(QWidget): # {{{ 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.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(':') @@ -1256,14 +1283,24 @@ class TagBrowserWidget(QWidget): # {{{ 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(): - warning_dialog(self.tags_view, _('No item found'), - _('No (more) matches for that search')).exec_() - + self.not_found_label.setVisible(True) + 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(4, 10) + self.not_found_label_timer.start(2000) + 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 b2d8e4b8fd..bc3c23876f 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -551,7 +551,11 @@ class HistoryLineEdit(QComboBox): item = unicode(self.itemText(i)) if item not in items: items.append(item) - + self.blockSignals(True) + self.clear() + self.addItems(items) + self.setEditText(ct) + self.blockSignals(False) history.set(self.store_name, items) def setText(self, t): 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 @@ class SendEmail(QWidget, Ui_Form): 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) diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index 02789c32de..5f771a8a6d 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -148,7 +148,6 @@ class CSV_XML(CatalogPlugin): outfile.close() elif self.fmt == 'xml': - #from lxml import etree from lxml.builder import E root = E.calibredb() @@ -1809,10 +1808,11 @@ class EPUB_MOBI(CatalogPlugin): if not self.useSeriesPrefixInTitlesSection: title_list = self.booksByTitle_noSeriesPrefix drtc = 0 + divRunningTag = None for book in title_list: if self.letter_or_symbol(book['title_sort'][0]) != current_letter : # Start a new letter - if drtc: + if drtc and divRunningTag is not None: divTag.insert(dtc, divRunningTag) dtc += 1 divRunningTag = Tag(soup, 'div') @@ -1874,12 +1874,14 @@ class EPUB_MOBI(CatalogPlugin): pBookTag.insert(ptc, emTag) ptc += 1 - divRunningTag.insert(drtc, pBookTag) + if divRunningTag is not None: + divRunningTag.insert(drtc, pBookTag) drtc += 1 # Add the last divRunningTag to divTag - divTag.insert(dtc, divRunningTag) - dtc += 1 + if divRunningTag is not None: + divTag.insert(dtc, divRunningTag) + dtc += 1 # Add the divTag to the body body.insert(btc, divTag) @@ -1938,6 +1940,7 @@ class EPUB_MOBI(CatalogPlugin): # Loop through booksByAuthor book_count = 0 + divRunningTag = None for book in self.booksByAuthor: book_count += 1 if self.letter_or_symbol(book['author_sort'][0].upper()) != current_letter : @@ -1964,7 +1967,7 @@ class EPUB_MOBI(CatalogPlugin): # Add divOpeningTag to divTag divTag.insert(dtc, divOpeningTag) dtc += 1 - elif author_count > 2: + elif author_count > 2 and divRunningTag is not None: divTag.insert(dtc, divRunningTag) dtc += 1 @@ -1983,7 +1986,7 @@ class EPUB_MOBI(CatalogPlugin): if author_count == 1: divOpeningTag.insert(dotc, pAuthorTag) dotc += 1 - else: + elif divRunningTag is not None: divRunningTag.insert(drtc,pAuthorTag) drtc += 1 @@ -2008,7 +2011,7 @@ class EPUB_MOBI(CatalogPlugin): if author_count == 1: divOpeningTag.insert(dotc, pSeriesTag) dotc += 1 - else: + elif divRunningTag is not None: divRunningTag.insert(drtc,pSeriesTag) drtc += 1 if current_series and not book['series']: @@ -2056,7 +2059,7 @@ class EPUB_MOBI(CatalogPlugin): if author_count == 1: divOpeningTag.insert(dotc, pBookTag) dotc += 1 - else: + elif divRunningTag is not None: divRunningTag.insert(drtc,pBookTag) drtc += 1 @@ -4048,9 +4051,13 @@ class EPUB_MOBI(CatalogPlugin): field, index_is_id=True) if field_contents: - if re.search(pat, unicode(field_contents), - re.IGNORECASE) is not None: - return True + try: + if re.search(pat, unicode(field_contents), + re.IGNORECASE) is not None: + return True + except: + # Compiling of pat failed, ignore it + pass return False 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 @@ books_series_link feeds (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 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 @@ class TemplateFormatter(string.Formatter): 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)