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/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index a9ba22f768..6914634d94 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,20 @@ 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) + width = self.not_found_label.parent().width()-8 + 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/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)